diff --git a/.eslintignore b/.eslintignore index 024d2614ef..1b8324e1e8 100755 --- a/.eslintignore +++ b/.eslintignore @@ -314,15 +314,10 @@ src/components/material/MaterialList.tsx src/components/material/MaterialView.tsx src/components/material/UpdateMaterial.tsx src/components/notifications/AddNotification.tsx -src/components/notifications/ConfigurationTab.component.tsx src/components/notifications/CreateHeaderDetails.tsx src/components/notifications/ModifyRecipientsModal.tsx src/components/notifications/NotificationTab.tsx src/components/notifications/Notifications.tsx -src/components/notifications/SESConfigModal.tsx -src/components/notifications/SMTPConfigModal.tsx -src/components/notifications/SlackConfigModal.tsx -src/components/notifications/WebhookConfigModal.tsx src/components/notifications/notifications.service.ts src/components/notifications/notifications.util.tsx src/components/onboardingGuide/GuideCommonHeader.tsx diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx index 549f1ea7ef..2088b7f5ac 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx @@ -238,10 +238,10 @@ export const ClusterEnvironmentDrawer = ({
-
+
{ }, isRequiredField = false, + autoFocus = false, }: ProtectedInputType) => { + const inputRef = useRef() + + useEffect(() => { + setTimeout(() => { + // Added timeout to ensure the autofocus code is executed post the re-renders + if (inputRef.current && autoFocus) { + inputRef.current.focus() + } + }, 100) + }, [autoFocus]) + const [shown, toggleShown] = useState(false) useEffect(() => { toggleShown(!hidden) @@ -871,7 +883,7 @@ export const ProtectedInput = ({ data-testid={dataTestid} type={shown ? 'text' : 'password'} tabIndex={tabIndex} - className={error ? 'form__input form__input--error pl-42' : 'form__input pl-42'} + className={error ? 'form__input form__input--error pl-42' : 'form__input pl-42 fs-13'} name={name} placeholder={placeholder} onChange={(e) => { @@ -881,6 +893,8 @@ export const ProtectedInput = ({ value={value} disabled={disabled} onBlur={onBlur} + autoFocus={autoFocus} + ref={inputRef} /> ) => void isRequiredField?: boolean + autoFocus?: boolean } diff --git a/src/components/notifications/AddNotification.tsx b/src/components/notifications/AddNotification.tsx index 29ed2c9469..0fc1ee4ea9 100644 --- a/src/components/notifications/AddNotification.tsx +++ b/src/components/notifications/AddNotification.tsx @@ -35,7 +35,7 @@ import { RouteComponentProps, Link } from 'react-router-dom' import { components } from 'react-select' import Tippy from '@tippyjs/react' import CreatableSelect from 'react-select/creatable' -import { SESConfigModal } from './SESConfigModal' +import SESConfigModal from './SESConfigModal' import { SlackConfigModal } from './SlackConfigModal' import { Select, validateEmail, ErrorBoundary } from '../common' import { ReactComponent as Slack } from '../../assets/icons/slack-logo.svg' @@ -985,7 +985,7 @@ export class AddNotification extends Component { + closeSESConfigModal={() => { this.setState({ showSESConfigModal: false }) }} /> @@ -1011,7 +1011,7 @@ export class AddNotification extends Component { + closeSMTPConfigModal={() => { this.setState({ showSMTPConfigModal: false }) }} /> diff --git a/src/components/notifications/ConfigTableRowActionButton.tsx b/src/components/notifications/ConfigTableRowActionButton.tsx new file mode 100644 index 0000000000..6ef861e6f7 --- /dev/null +++ b/src/components/notifications/ConfigTableRowActionButton.tsx @@ -0,0 +1,32 @@ +import { Trash } from '@Components/common' +import { ReactComponent as Edit } from '@Icons/ic-pencil.svg' +import { Button, ButtonStyleType, ButtonVariantType, ComponentSizeType } from '@devtron-labs/devtron-fe-common-lib' +import { ConfigTableRowActionButtonProps } from './types' + +export const ConfigTableRowActionButton = ({ + rootClassName, + onClickEditRow, + onClickDeleteRow, + modal, +}: ConfigTableRowActionButtonProps) => ( +
+
+) diff --git a/src/components/notifications/ConfigurationDrawerModal.tsx b/src/components/notifications/ConfigurationDrawerModal.tsx new file mode 100644 index 0000000000..60150de878 --- /dev/null +++ b/src/components/notifications/ConfigurationDrawerModal.tsx @@ -0,0 +1,78 @@ +import { + Button, + ButtonStyleType, + ButtonVariantType, + ComponentSizeType, + Drawer, + Progressing, +} from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as Close } from '@Icons/ic-close.svg' +import { ConfigurationsTabTypes } from './constants' +import { ConfigurationTabDrawerModalProps } from './types' +import { getTabText } from './notifications.util' + +export const ConfigurationTabDrawerModal = ({ + renderContent, + closeModal, + modal, + isLoading, + saveConfigModal, + disableSave, +}: ConfigurationTabDrawerModalProps) => { + const renderFooter = () => ( +
+
+ ) + + const renderModalContent = () => { + if (isLoading) { + return + } + return ( +
+ {renderContent()} + {renderFooter()} +
+ ) + } + + return ( + +
+
+

Configure {getTabText(modal)}

+
+ {renderModalContent()} +
+
+ ) +} diff --git a/src/components/notifications/ConfigurationTab.tsx b/src/components/notifications/ConfigurationTab.tsx index 4caf80ff65..5219ce2716 100644 --- a/src/components/notifications/ConfigurationTab.tsx +++ b/src/components/notifications/ConfigurationTab.tsx @@ -15,10 +15,17 @@ */ import { useEffect, useState } from 'react' -import { showError, Progressing, ErrorScreenNotAuthorized, DeleteComponent } from '@devtron-labs/devtron-fe-common-lib' -import { Route, Switch, useHistory, useRouteMatch } from 'react-router-dom' +import { + showError, + Progressing, + useSearchString, + ConfirmationModal, + ConfirmationModalVariantType, + ServerErrors, +} from '@devtron-labs/devtron-fe-common-lib' +import { Route, Switch, useHistory, useLocation, useRouteMatch } from 'react-router-dom' import { SlackConfigModal } from './SlackConfigModal' -import { SESConfigModal } from './SESConfigModal' +import SESConfigModal from './SESConfigModal' import { deleteNotification, getSESConfiguration, @@ -27,126 +34,119 @@ import { getSMTPConfiguration, getWebhookConfiguration, } from './notifications.service' -import { ViewType } from '../../config/constants' import { DC_CONFIGURATION_CONFIRMATION_MESSAGE, DeleteComponentsName } from '../../config/constantMessaging' import { SMTPConfigModal } from './SMTPConfigModal' import { WebhookConfigModal } from './WebhookConfigModal' import { ConfigurationTabState } from './types' -import { SlackConfigurationTable } from './SlackConfigurationTable' +import SlackConfigurationTable from './SlackConfigurationTable' import { WebhookConfigurationTable } from './WebhookConfigurationTable' -import { SESConfigurationTable } from './SESConfigurationTable' +import SESConfigurationTable from './SESConfigurationTable' import { SMTPConfigurationTable } from './SMTPConfigurationTable' import { ConfigurationTabSwitcher } from './ConfigurationTabsSwitcher' -import { ConfigurationsTabTypes } from './constants' +import { ConfigurationFieldKeys, ConfigurationsTabTypes, ConfigurationTabText } from './constants' export const ConfigurationTab = () => { const { path } = useRouteMatch() const history = useHistory() + const location = useLocation() + const { searchParams } = useSearchString() + const queryString = new URLSearchParams(location.search) + const modal = queryString.get('modal') + const [state, setState] = useState({ - view: ViewType.LOADING, - showSlackConfigModal: false, - showSESConfigModal: false, - showSMTPConfigModal: false, - showWebhookConfigModal: false, - slackConfigId: 0, - sesConfigId: 0, - smtpConfigId: 0, - webhookConfigId: 0, sesConfigurationList: [], smtpConfigurationList: [], slackConfigurationList: [], webhookConfigurationList: [], abortAPI: false, - deleting: false, confirmation: false, sesConfig: {}, smtpConfig: {}, slackConfig: {}, webhookConfig: {}, - showDeleteConfigModalType: '', activeTab: ConfigurationsTabTypes.SES, + isLoading: false, + showCannotDeleteDialogModal: false, }) - const getAllChannelConfigs = (): void => { - getConfigs() - .then((response) => { - setState({ - ...state, - slackConfigurationList: response.result.slackConfigurationList, - sesConfigurationList: response.result.sesConfigurationList, - smtpConfigurationList: response.result.smtpConfigurationList, - webhookConfigurationList: response.result.webhookConfigurationList, - view: ViewType.FORM, - }) - }) - .catch((error) => { - showError(error, true, true) - setState({ ...state, view: ViewType.ERROR }) + const getAllChannelConfigs = async () => { + setState({ ...state, isLoading: true }) + try { + const { result } = await getConfigs() + setState({ + ...state, + slackConfigurationList: result.slackConfigurationList, + sesConfigurationList: result.sesConfigurationList, + smtpConfigurationList: result.smtpConfigurationList, + webhookConfigurationList: result.webhookConfigurationList, + isLoading: false, + confirmation: false, }) + } catch (error) { + showError(error, true, true) + setState({ ...state, isLoading: false }) + } } + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises getAllChannelConfigs() - history.push(`${path}/ses`) - }, []) - const onSaveWebhook = () => { - setState({ ...state, showWebhookConfigModal: false, webhookConfigId: 0 }) - getAllChannelConfigs() - } + const newParams = { + ...searchParams, + modal: modal ?? ConfigurationsTabTypes.SES, + } - const onCloseWebhookModal = () => { - setState({ ...state, showWebhookConfigModal: false, webhookConfigId: 0 }) - } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) + }, []) - const deleteClickHandler = async (configId, type) => { + const deleteClickHandler = (configId, type: ConfigurationsTabTypes) => async () => { try { - if (type === DeleteComponentsName.SlackConfigurationTab) { + if (type === ConfigurationsTabTypes.SLACK) { const { result } = await getSlackConfiguration(configId, true) setState({ ...state, - slackConfigId: configId, slackConfig: { ...result, - channel: DeleteComponentsName.SlackConfigurationTab, + channel: ConfigurationsTabTypes.SLACK, }, confirmation: true, - showDeleteConfigModalType: DeleteComponentsName.SlackConfigurationTab, + activeTab: ConfigurationsTabTypes.SLACK, }) - } else if (type === DeleteComponentsName.SesConfigurationTab) { + } else if (type === ConfigurationsTabTypes.SES) { const { result } = await getSESConfiguration(configId) setState({ ...state, - sesConfigId: configId, sesConfig: { ...result, - channel: DeleteComponentsName.SesConfigurationTab, + channel: ConfigurationsTabTypes.SES, }, confirmation: true, - showDeleteConfigModalType: DeleteComponentsName.SesConfigurationTab, + activeTab: ConfigurationsTabTypes.SES, }) - } else if (type === DeleteComponentsName.SMTPConfigurationTab) { + } else if (type === ConfigurationsTabTypes.SMTP) { const { result } = await getSMTPConfiguration(configId) setState({ ...state, - smtpConfigId: configId, smtpConfig: { ...result, - channel: DeleteComponentsName.SMTPConfigurationTab, + channel: ConfigurationsTabTypes.SMTP, }, confirmation: true, - showDeleteConfigModalType: DeleteComponentsName.SMTPConfigurationTab, + activeTab: ConfigurationsTabTypes.SMTP, }) - } else if (type === DeleteComponentsName.WebhookConfigurationTab) { + } else if (type === ConfigurationsTabTypes.WEBHOOK) { const { result } = await getWebhookConfiguration(configId) setState({ ...state, - webhookConfigId: configId, webhookConfig: { ...result, channel: DeleteComponentsName.WebhookConfigurationTab, }, confirmation: true, - showDeleteConfigModalType: DeleteComponentsName.WebhookConfigurationTab, + activeTab: ConfigurationsTabTypes.WEBHOOK, }) } } catch (e) { @@ -154,172 +154,155 @@ export const ConfigurationTab = () => { } } - const renderSlackConfigurationTable = () => ( - - ) - - const renderWebhookConfigurationTable = () => ( - - ) - - const renderSESConfigurationTable = () => ( - - ) - - const renderSMTPConfigurationTable = () => ( - - ) - - const setDeleting = () => { + const hideDeleteModal = () => { setState({ ...state, - deleting: true, + confirmation: false, }) } - const toggleConfirmation = (confirmation) => { - setState({ - ...state, - confirmation, - ...(!confirmation && { showDeleteConfigModalType: '' }), - }) + const deleteConfigPayload = (): any => { + const { activeTab, slackConfig, sesConfig, webhookConfig, smtpConfig } = state + if (activeTab === ConfigurationsTabTypes.SLACK) { + return slackConfig + } + if (activeTab === ConfigurationsTabTypes.SES) { + return sesConfig + } + if (activeTab === ConfigurationsTabTypes.WEBHOOK) { + return webhookConfig + } + return smtpConfig } - const renderSESConfigModal = () => { - const { showSESConfigModal, sesConfigId, sesConfigurationList } = state - if (!showSESConfigModal) return null - return ( - { - setState({ ...state, showSESConfigModal: false, sesConfigId: 0 }) - getAllChannelConfigs() - }} - closeSESConfigModal={() => { - setState({ ...state, showSESConfigModal: false }) - }} - /> - ) - } + const payload = deleteConfigPayload() - const renderSMTPConfigModal = () => { - const { showSMTPConfigModal, smtpConfigId, smtpConfigurationList } = state - if (!showSMTPConfigModal) return null + if (state.isLoading) { return ( - { - setState({ ...state, showSMTPConfigModal: false, smtpConfigId: 0 }) - getAllChannelConfigs() - }} - closeSMTPConfigModal={() => { - setState({ ...state, showSMTPConfigModal: false }) - }} - /> +
+ +
) } - const renderSlackConfigModal = () => { - const { showSlackConfigModal, slackConfigId } = state - if (!showSlackConfigModal) return null - return ( - { - setState({ ...state, showSlackConfigModal: false, slackConfigId: 0 }) - getAllChannelConfigs() - }} - closeSlackConfigModal={() => { - setState({ ...state, showSlackConfigModal: false, slackConfigId: 0 }) - }} - /> - ) + const renderModal = () => { + if (queryString.get(ConfigurationFieldKeys.CONFIG_ID) === null) return null + const configId = parseInt(queryString.get(ConfigurationFieldKeys.CONFIG_ID), 10) + + switch (modal) { + case ConfigurationsTabTypes.SES: + return ( + + ) + case ConfigurationsTabTypes.SMTP: + return ( + + ) + case ConfigurationsTabTypes.SLACK: + return + case ConfigurationsTabTypes.WEBHOOK: + return + default: + return null + } } - const renderWebhookConfigModal = () => { - const { showWebhookConfigModal, webhookConfigId } = state - if (!showWebhookConfigModal) return null - return ( - - ) + const reloadDeleteConfig = () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + getAllChannelConfigs() } - const deleteConfigPayload = (): any => { - const { showDeleteConfigModalType, slackConfig, sesConfig, webhookConfig, smtpConfig } = state - if (showDeleteConfigModalType === DeleteComponentsName.SlackConfigurationTab) { - return slackConfig + const renderTableComponent = () => { + switch (modal) { + case ConfigurationsTabTypes.SES: + return + case ConfigurationsTabTypes.SMTP: + return + case ConfigurationsTabTypes.SLACK: + return + case ConfigurationsTabTypes.WEBHOOK: + return + default: + return null } - if (showDeleteConfigModalType === DeleteComponentsName.SesConfigurationTab) { - return sesConfig - } - if (showDeleteConfigModalType === DeleteComponentsName.WebhookConfigurationTab) { - return webhookConfig - } - return smtpConfig } - const deleteConfigComponent = (): string => { - const { showDeleteConfigModalType } = state - if (showDeleteConfigModalType === DeleteComponentsName.SlackConfigurationTab) { - return DeleteComponentsName.SlackConfigurationTab - } - if (showDeleteConfigModalType === DeleteComponentsName.SesConfigurationTab) { - return DeleteComponentsName.SesConfigurationTab - } - if (showDeleteConfigModalType === DeleteComponentsName.WebhookConfigurationTab) { - return DeleteComponentsName.WebhookConfigurationTab + const onClickDelete = async () => { + try { + await deleteNotification(payload) + reloadDeleteConfig() + setState({ ...state, confirmation: false }) + } catch (serverError) { + if (serverError instanceof ServerErrors && serverError.code === 500) { + setState({ ...state, showCannotDeleteDialogModal: true }) + } else { + showError(serverError) + } + setState({ ...state, confirmation: false, showCannotDeleteDialogModal: true }) } - return DeleteComponentsName.SMTPConfigurationTab } - if (state.view === ViewType.LOADING) { - return ( -
- -
- ) + const handleConfirmation = () => { + setState({ ...state, showCannotDeleteDialogModal: false }) } - if (state.view === ViewType.ERROR) { - return ( -
- -
- ) + + const renderCannotDeleteDialogModal = () => ( + + ) + + if (state.isLoading) { + return } - const payload = deleteConfigPayload() + return ( -
- +
+ - - - - + - {renderSESConfigModal()} - {renderSMTPConfigModal()} - {renderSlackConfigModal()} - {renderWebhookConfigModal()} + {renderModal()} + + - {state.confirmation && ( - - )} + {state.showCannotDeleteDialogModal && renderCannotDeleteDialogModal()}
) } diff --git a/src/components/notifications/ConfigurationTabsSwitcher.tsx b/src/components/notifications/ConfigurationTabsSwitcher.tsx index 340ee461cf..a10907f343 100644 --- a/src/components/notifications/ConfigurationTabsSwitcher.tsx +++ b/src/components/notifications/ConfigurationTabsSwitcher.tsx @@ -1,60 +1,59 @@ -import { NavLink, useRouteMatch } from 'react-router-dom' -import { Button, ButtonVariantType, ComponentSizeType } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' +import { Button, ButtonVariantType, ComponentSizeType, useSearchString } from '@devtron-labs/devtron-fe-common-lib' import { ReactComponent as Add } from '@Icons/ic-add.svg' -import { getConfigurationTabTextWithIcon } from './notifications.util' -import { ConfigurationTabSwitcherProps } from './types' +import { getConfigurationTabTextWithIcon, getTabText } from './notifications.util' import { ConfigurationsTabTypes } from './constants' -export const ConfigurationTabSwitcher = ({ activeTab, setState, state }: ConfigurationTabSwitcherProps) => { - const match = useRouteMatch() +export const ConfigurationTabSwitcher = () => { + const history = useHistory() + const { searchParams } = useSearchString() + const queryParams = new URLSearchParams(history.location.search) + const activeTab = queryParams.get('modal') as ConfigurationsTabTypes - const handleTabClick = (tab: ConfigurationsTabTypes) => { - setState({ ...state, activeTab: tab }) + const handleTabClick = (tab: ConfigurationsTabTypes) => () => { + const newParams = { + ...searchParams, + modal: tab, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) } const handleAddClick = () => { - switch (activeTab) { - case ConfigurationsTabTypes.SES: - setState({ ...state, showSESConfigModal: true, sesConfigId: 0 }) - break - case ConfigurationsTabTypes.SMTP: - setState({ ...state, showSMTPConfigModal: true, smtpConfigId: 0 }) - break - case ConfigurationsTabTypes.SLACK: - setState({ ...state, showSlackConfigModal: true, slackConfigId: 0 }) - break - case ConfigurationsTabTypes.WEBHOOK: - setState({ ...state, showWebhookConfigModal: true, webhookConfigId: 0 }) - break - default: - break + const newParams = { + ...searchParams, + modal: activeTab, + configId: '0', } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) } return ( -
-
- {getConfigurationTabTextWithIcon().map((tab) => ( - +
+ {getConfigurationTabTextWithIcon().map((tab, index) => ( + ))}
) diff --git a/src/components/notifications/CreateHeaderDetails.tsx b/src/components/notifications/CreateHeaderDetails.tsx deleted file mode 100644 index 3eda97179b..0000000000 --- a/src/components/notifications/CreateHeaderDetails.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed 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 React from 'react' -import { CustomInput } from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as DeleteCross } from '../../assets/icons/ic-cross.svg' -import { CreateHeaderDetailsType } from './types' - -export default function CreateHeaderDetails({ - index, - headerData, - setHeaderData, - removeHeader, -}: CreateHeaderDetailsType) { - const deleteHeader = (e): void => { - e.stopPropagation() - removeHeader(index) - } - - const handleInputChange = (e): void => { - const _headerData = { ...headerData } - _headerData[e.target.name] = e.target.value - setHeaderData(index, _headerData) - } - - return ( -
- - - -
- -
-
- ) -} diff --git a/src/components/notifications/DefaultCheckbox.tsx b/src/components/notifications/DefaultCheckbox.tsx new file mode 100644 index 0000000000..5bd1b444b0 --- /dev/null +++ b/src/components/notifications/DefaultCheckbox.tsx @@ -0,0 +1,16 @@ +import { Checkbox, CHECKBOX_VALUE } from '@devtron-labs/devtron-fe-common-lib' +import { DefaultCheckboxProps } from './types' + +export const DefaultCheckbox = ({ shouldBeDefault, handleCheckbox, isDefault }: DefaultCheckboxProps) => ( + + Set as default configuration to send emails + +) diff --git a/src/components/notifications/EmptyConfigurationView.tsx b/src/components/notifications/EmptyConfigurationView.tsx index 7a1139af67..da51be417e 100644 --- a/src/components/notifications/EmptyConfigurationView.tsx +++ b/src/components/notifications/EmptyConfigurationView.tsx @@ -2,9 +2,11 @@ import { GenericEmptyState } from '@devtron-labs/devtron-fe-common-lib' import { EmptyConfigurationSubTitle } from './constants' import { EmptyConfigurationViewProps } from './types' -export const EmptyConfigurationView = ({ configTabType }: EmptyConfigurationViewProps) => ( +export const EmptyConfigurationView = ({ configTabType, image }: EmptyConfigurationViewProps) => ( ) diff --git a/src/components/notifications/Notifications.tsx b/src/components/notifications/Notifications.tsx index 922ef7bfb4..d4da9c3310 100644 --- a/src/components/notifications/Notifications.tsx +++ b/src/components/notifications/Notifications.tsx @@ -30,7 +30,7 @@ interface NotificationsProps extends RouteComponentProps<{}> { export default class Notifications extends Component { renderNotificationHeader() { return ( -
+
void - onSaveSuccess: () => void - closeSESConfigModal: (event) => void -} +const SESConfigModal = ({ + sesConfigId, + shouldBeDefault, + selectSESFromChild, + onSaveSuccess, + closeSESConfigModal, +}: SESConfigModalProps) => { + const history = useHistory() + const selectRef = useRef(null) -export interface SESConfigModalState { - view: string - form: { - configName: string - accessKey: string - secretKey: string - region: { label: string; value: string } - fromEmail: string - default: boolean - isLoading: boolean - isError: boolean - } - isValid: { - configName: boolean - accessKey: boolean - secretKey: boolean - region: boolean - fromEmail: boolean - } - secretKey: string -} + const [form, setForm] = useState(getSESDefaultConfiguration(shouldBeDefault)) + const [isFormValid, setFormValid] = useState(DefaultSESValidations) -export class SESConfigModal extends Component { - awsRegionListParsed = awsRegionList.map((region) => { - return { label: region.name, value: region.value } - }) + const awsRegionListParsed = awsRegionList + .sort((a, b) => stringComparatorBySortOrder(a.name, b.name)) + .map((region) => ({ label: region.name, value: region.value })) - constructor(props) { - super(props) - this.state = { - view: ViewType.LOADING, - form: { - configName: '', - accessKey: '', - secretKey: '', - region: { label: '', value: '' }, - fromEmail: '', - default: this.props.shouldBeDefault, - isLoading: false, - isError: true, - }, - isValid: { - configName: true, - accessKey: true, - secretKey: true, - region: true, - fromEmail: true, - }, - secretKey: '', - } - this.handleConfigNameChange = this.handleConfigNameChange.bind(this) - this.handleAWSRegionChange = this.handleAWSRegionChange.bind(this) - this.handleAccessKeyIDChange = this.handleAccessKeyIDChange.bind(this) - this.handleSecretAccessKeyChange = this.handleSecretAccessKeyChange.bind(this) - this.handleEmailChange = this.handleEmailChange.bind(this) - this.handleCheckbox = this.handleCheckbox.bind(this) - this.handleBlur = this.handleBlur.bind(this) - this.onSaveClickHandler = this.onSaveClickHandler.bind(this) - } + const fetchSESConfiguration = async () => { + setForm((prevForm) => ({ ...prevForm, isLoading: true })) + try { + const response = await getSESConfiguration(sesConfigId) + const { region } = response.result + const awsRegion = awsRegionListParsed.find((r) => r.value === region) - componentDidMount() { - if (this.props.sesConfigId) { - getSESConfiguration(this.props.sesConfigId) - .then((response) => { - const state = { ...this.state } - const { region } = response.result - const awsRegion = this.awsRegionListParsed.find((r) => r.value === region) - state.form = { - ...response.result, - isLoading: false, - isError: true, - region: awsRegion, - secretKey: '*******', - } - state.view = ViewType.FORM - state.isValid = { - configName: true, - accessKey: true, - secretKey: true, - region: true, - fromEmail: true, - } - state.secretKey = response.result.secretKey - this.setState(state) - }) - .catch((error) => { - showError(error) - }) - } else { - const state = { ...this.state } - state.form.default = this.props.shouldBeDefault - state.view = ViewType.FORM - this.setState(state) + setForm({ + ...response.result, + isLoading: false, + region: awsRegion, + secretKey: DEFAULT_SECRET_PLACEHOLDER, // Masked secretKey for security + }) + setFormValid(DefaultSESValidations) + } catch (error) { + showError(error) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) } } - handleBlur(event, key: string): void { - const { isValid } = { ...this.state } - if (key !== 'region') { - isValid[key] = !!event.target.value.length - } else { - isValid[key] = !!this.state.form.region.value + useEffect(() => { + if (sesConfigId) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchSESConfiguration() } - this.setState({ isValid }) - } + }, [sesConfigId]) - handleConfigNameChange(event: React.ChangeEvent): void { - const { form } = { ...this.state } - form.configName = event.target.value - this.setState({ form }) + const handleInputChange = (e) => { + const { name, value } = e.target + setForm((prevForm) => ({ + ...prevForm, + [name]: value, + })) + setFormValid((prevValid) => ({ + ...prevValid, + [name]: validateKeyValueConfig(name, value), + })) } - - handleAccessKeyIDChange(event: React.ChangeEvent): void { - const { form, isValid } = { ...this.state } - form.accessKey = event.target.value - this.setState({ form, isValid }) + const handleBlur = (event): void => { + const { name, value } = event.target + setFormValid((prevValid) => ({ + ...prevValid, + [name]: validateKeyValueConfig(name as ConfigurationFieldKeys, value), + })) } - handleSecretAccessKeyChange(event: React.ChangeEvent): void { - const { form, isValid } = { ...this.state } - let { secretKey } = this.state - form.secretKey = event.target.value - if (event.target.value.indexOf('*') < 0 && event.target.value.length > 0) { - secretKey = event.target.value - } - this.setState({ form, isValid, secretKey }) + const handleAWSRegionChange = (selected: OptionType): void => { + setFormValid((prevValid) => ({ + ...prevValid, + region: validateKeyValueConfig(ConfigurationFieldKeys.REGION, selected.value), + })) + setForm((prevForm) => ({ + ...prevForm, + region: selected, + })) } - handleAWSRegionChange(event): void { - const { form, isValid } = { ...this.state } - form.region = event - isValid.region = !!event - this.setState({ form, isValid }) + const handleAWSBlur: SelectPickerProps['onBlur'] = (): void => { + const selectedValue = selectRef.current?.getValue()[0] || {} + setFormValid((prevValid) => ({ + ...prevValid, + region: validateKeyValueConfig(ConfigurationFieldKeys.REGION, selectedValue.value), + })) } - handleEmailChange(event: React.ChangeEvent): void { - const { form, isValid } = { ...this.state } - form.fromEmail = event.target.value - this.setState({ form, isValid }) + const handleCheckbox = (): void => { + setForm((prevForm) => ({ + ...prevForm, + default: !prevForm.default, + })) } - handleCheckbox(event): void { - const { form, isValid } = { ...this.state } - form.default = !form.default - this.setState({ form, isValid }) - } + const getPayload = () => ({ + ...form, + region: form.region.value, + }) - getPayload = () => { - return { - ...this.state.form, - region: this.state.form.region.value, - secretKey: this.state.secretKey, + const closeSESConfig = () => { + if (typeof closeSESConfigModal === 'function') { + closeSESConfigModal() + } else { + const newParams = { + modal: ConfigurationsTabTypes.SES, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) } } - saveSESConfig(): void { - const keys = Object.keys(this.state.isValid) - let isFormValid = keys.reduce((isFormValid, key) => { - isFormValid = isFormValid && this.state.isValid[key] - return isFormValid - }, true) - isFormValid = isFormValid && validateEmail(this.state.form.fromEmail) - if (!isFormValid) { - const state = { ...this.state } - state.form.isLoading = false - state.form.isError = true - this.setState(state) - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Some required fields are missing or Invalid', + const getAllFieldsValidated = () => { + const { configName, accessKey, secretKey, region, fromEmail } = form + return ( + !!configName && + !!accessKey && + !!secretKey && + !!region && + !!fromEmail && + getFormValidated(isFormValid, fromEmail) + ) + } + const saveSESConfig = async () => { + if (!getAllFieldsValidated()) { + setForm((prevForm) => ({ + ...prevForm, + isLoading: false, + isError: true, + })) + setFormValid({ + ...isFormValid, + configName: validateKeyValueConfig(ConfigurationFieldKeys.CONFIG_NAME, form.configName), + accessKey: validateKeyValueConfig(ConfigurationFieldKeys.ACCESS_KEY, form.accessKey), + secretKey: validateKeyValueConfig(ConfigurationFieldKeys.SECRET_KEY, form.secretKey), + region: validateKeyValueConfig(ConfigurationFieldKeys.REGION, form.region?.value?.toString()), + fromEmail: validateKeyValueConfig(ConfigurationFieldKeys.FROM_EMAIL, form.fromEmail), }) + renderErrorToast() return } - const state = { ...this.state } - state.form.isLoading = true - state.form.isError = false - this.setState(state) - saveEmailConfiguration(this.getPayload(), 'ses') - .then((response) => { - const state = { ...this.state } - state.form.isLoading = false - this.setState(state) - ToastManager.showToast({ - variant: ToastVariantType.success, - description: 'Saved Successfully', - }) - this.props.onSaveSuccess() - if (this.props.selectSESFromChild) { - this.props.selectSESFromChild(response?.result[0]) - } - }) - .catch((error) => { - showError(error) - const state = { ...this.state } - state.form.isLoading = false - this.setState(state) + try { + const response = await saveEmailConfiguration(getPayload(), ConfigurationsTabTypes.SES) + ToastManager.showToast({ + variant: ToastVariantType.success, + description: 'Saved Successfully', }) + if (selectSESFromChild) selectSESFromChild(response?.result[0]) + onSaveSuccess() + closeSESConfig() + } catch (error) { + showError(error) + } finally { + setForm((prevForm) => ({ ...prevForm, isLoading: false })) + } } - renderWithBackdrop(body) { - return ( - -
-
-

Configure SES

- -
- {body} -
-
- ) - } + const renderSESContent = () => ( +
+ + + + - onSaveClickHandler(event) { - event.preventDefault() - this.saveSESConfig() - } + + +
+ ) - render() { - let body - if (this.state.view === ViewType.LOADING) { - body = ( -
- -
- ) - } else { - body = ( - <> -
- - - -
- this.handleBlur(event, 'region')} - onChange={(selected) => this.handleAWSRegionChange(selected)} - options={this.awsRegionListParsed} - size={ComponentSizeType.large} - /> - - {!this.state.isValid.region ? ( - <> - - This is a required field
- - ) : null} -
-
- -
-
- - Set as default configuration to send emails - -
- - -
-
- - ) - } - return this.renderWithBackdrop(body) - } + return ( + + ) } + +export default SESConfigModal diff --git a/src/components/notifications/SESConfigurationTable.tsx b/src/components/notifications/SESConfigurationTable.tsx index 681c033030..f4e835a7a2 100644 --- a/src/components/notifications/SESConfigurationTable.tsx +++ b/src/components/notifications/SESConfigurationTable.tsx @@ -1,96 +1,71 @@ -import { Trash } from '@Components/common' import { DeleteComponentsName } from '@Config/constantMessaging' -import { ViewType } from '@Config/constants' -import { Progressing } from '@devtron-labs/devtron-fe-common-lib' -import Tippy from '@tippyjs/react' -import { ReactComponent as Edit } from '@Icons/ic-edit.svg' +import { useSearchString } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' import { ConfigurationTableProps } from './types' import { EmptyConfigurationView } from './EmptyConfigurationView' import { ConfigurationsTabTypes } from './constants' +import { getConfigTabIcons, renderDefaultTag, renderText } from './notifications.util' +import './notifications.scss' +import emptySES from '../../assets/img/ses-empty.png' +import { ConfigTableRowActionButton } from './ConfigTableRowActionButton' + +const SESConfigurationTable = ({ state, deleteClickHandler }: ConfigurationTableProps) => { + const { searchParams } = useSearchString() + const history = useHistory() + + const { sesConfigurationList } = state -export const SESConfigurationTable = ({ setState, state, deleteClickHandler }: ConfigurationTableProps) => { - const { view, sesConfigurationList } = state - if (view === ViewType.LOADING) { - return ( -
- -
- ) - } if (sesConfigurationList.length === 0) { - return + return + } + + const onClickSESConfigEdit = (id: number) => () => { + const newParams = { + ...searchParams, + configId: id.toString(), + modal: ConfigurationsTabTypes.SES, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) } + return ( - - - - - - - - - - - {sesConfigurationList.map((sesConfig) => ( - - ))} - - -
NameAccess key IdSender' Email -
-
- {sesConfig.name} - {sesConfig.isDefault ? ( - Default - ) : null} -
-
- {sesConfig.accessKeyId} -
-
{sesConfig.email}
-
- - - - - - -
-
+
+
+

+

Name

+

Access Key Id

+

Sender's Email

+

+

+
+ {sesConfigurationList.map((sesConfig) => ( +
+ {getConfigTabIcons(ConfigurationsTabTypes.SES)} +
+ {renderText(sesConfig.name, true, onClickSESConfigEdit(sesConfig.id))} + {renderDefaultTag(sesConfig.isDefault)} +
+ {renderText(sesConfig.accessKeyId)} + {renderText(sesConfig.email)} + +
+ ))} +
+
) } + +export default SESConfigurationTable diff --git a/src/components/notifications/SMTPConfigModal.tsx b/src/components/notifications/SMTPConfigModal.tsx index 5f89c396a5..ae0296c020 100644 --- a/src/components/notifications/SMTPConfigModal.tsx +++ b/src/components/notifications/SMTPConfigModal.tsx @@ -13,309 +13,224 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import React, { Component } from 'react' -import { - showError, - Progressing, - Checkbox, - Drawer, - CustomInput, - CHECKBOX_VALUE, - ToastManager, - ToastVariantType, -} from '@devtron-labs/devtron-fe-common-lib' -import { validateEmail } from '../common' +import { useState, useEffect } from 'react' +import { showError, CustomInput, ToastManager, ToastVariantType } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' import { getSMTPConfiguration, saveEmailConfiguration } from './notifications.service' -import { ReactComponent as Close } from '../../assets/icons/ic-close.svg' -import { ViewType } from '../../config/constants' import { ProtectedInput } from '../globalConfigurations/GlobalConfiguration' -import { SMTPConfigModalProps, SMTPConfigModalState } from './types' -import { REQUIRED_FIELD_MSG } from '../../config/constantMessaging' +import { ConfigurationFieldKeys, ConfigurationsTabTypes, DefaultSMTPValidation } from './constants' +import { SMTPConfigModalProps, SMTPFormType } from './types' +import { ConfigurationTabDrawerModal } from './ConfigurationDrawerModal' +import { getFormValidated, getSMTPDefaultConfiguration, validateKeyValueConfig } from './notifications.util' +import { DefaultCheckbox } from './DefaultCheckbox' + +export const SMTPConfigModal = ({ + smtpConfigId, + shouldBeDefault, + closeSMTPConfigModal, + onSaveSuccess, + selectSMTPFromChild, +}: SMTPConfigModalProps) => { + const history = useHistory() -export class SMTPConfigModal extends Component { - constructor(props) { - super(props) - this.state = { - view: ViewType.LOADING, - form: { - configName: '', - port: null, - host: '', - authUser: '', - authPassword: '', - fromEmail: '', - default: this.props.shouldBeDefault, + const [form, setForm] = useState(getSMTPDefaultConfiguration(shouldBeDefault)) + const [isFormValid, setFormValid] = useState(DefaultSMTPValidation) + + const fetchSMTPConfig = async () => { + setForm((prevForm) => ({ ...prevForm, isLoading: true })) + try { + const response = await getSMTPConfiguration(smtpConfigId) + setForm({ + ...response.result, isLoading: false, - isError: true, - }, - isValid: { - configName: true, - port: true, - host: true, - authUser: true, - authPassword: true, - fromEmail: true, - }, + port: response.result.port.toString(), + }) + setFormValid(DefaultSMTPValidation) + } catch (error) { + showError(error) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) } - this.handleCheckbox = this.handleCheckbox.bind(this) - this.handleBlur = this.handleBlur.bind(this) - this.handleInputChange = this.handleInputChange.bind(this) - this.onSaveClickHandler = this.onSaveClickHandler.bind(this) } - componentDidMount() { - if (this.props.smtpConfigId) { - getSMTPConfiguration(this.props.smtpConfigId) - .then((response) => { - this.setState((prevState) => ({ - ...prevState, - form: { ...response.result, isLoading: false, isError: true }, - view: ViewType.FORM, - isValid: { - configName: true, - port: true, - host: true, - authUser: true, - authPassword: true, - fromEmail: true, - }, - })) - }) - .catch((error) => { - showError(error) - }) + useEffect(() => { + if (smtpConfigId) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchSMTPConfig() } else { - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, default: this.props.shouldBeDefault }, - view: ViewType.FORM, - })) + setForm((prevForm) => ({ ...prevForm, default: shouldBeDefault })) } + }, [smtpConfigId, shouldBeDefault]) + + const handleBlur = (e) => { + const { name, value } = e.target + setFormValid((prevValid) => ({ ...prevValid, [name]: validateKeyValueConfig(name, value) })) } - handleBlur(event): void { - const { name, value } = event.target - this.setState((prevState) => ({ - ...prevState, - isValid: { ...prevState.isValid, [name]: !!value.length }, - })) + const handleInputChange = (e) => { + const { name, value } = e.target + setForm((prevForm) => ({ ...prevForm, [name]: value })) + setFormValid((prevValid) => ({ ...prevValid, [name]: validateKeyValueConfig(name, value) })) } - handleInputChange(event: React.ChangeEvent): void { - const { name, value } = event.target - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, [name]: value }, - })) + const handleCheckbox = () => { + setForm((prevForm) => ({ ...prevForm, default: !prevForm.default })) } - handleCheckbox(): void { - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, default: !prevState.form.default }, - })) + const closeSMTPConfig = () => { + if (typeof closeSMTPConfigModal === 'function') { + closeSMTPConfigModal() + } else { + const newParams = { + modal: ConfigurationsTabTypes.SMTP, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) + } } - saveSMTPConfig(): void { - const keys = Object.keys(this.state.isValid) - let isFormValid = keys.reduce((isFormValid, key) => { - isFormValid = isFormValid && this.state.isValid[key] - return isFormValid - }, true) - isFormValid = isFormValid && validateEmail(this.state.form.fromEmail) - if (!isFormValid) { - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, isLoading: false, isError: true }, - })) + const getAllFieldsValidated = () => { + const { configName, host, port, authUser, authPassword, fromEmail } = form + return ( + !!configName && + !!host && + !!port && + !!authUser && + !!authPassword && + !!fromEmail && + getFormValidated(isFormValid, fromEmail) + ) + } + + const saveSMTPConfig = () => { + if (!getAllFieldsValidated()) { ToastManager.showToast({ variant: ToastVariantType.error, description: 'Some required fields are missing or Invalid', }) + setFormValid((prevValid) => ({ + ...prevValid, + configName: validateKeyValueConfig(ConfigurationFieldKeys.CONFIG_NAME, form.configName), + host: validateKeyValueConfig(ConfigurationFieldKeys.HOST, form.host), + port: validateKeyValueConfig(ConfigurationFieldKeys.PORT, form.port), + authUser: validateKeyValueConfig(ConfigurationFieldKeys.AUTH_USER, form.authUser), + authPassword: validateKeyValueConfig(ConfigurationFieldKeys.AUTH_PASSWORD, form.authPassword), + fromEmail: validateKeyValueConfig(ConfigurationFieldKeys.FROM_EMAIL, form.fromEmail), + })) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) return } - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, isLoading: true, isError: false }, - })) - saveEmailConfiguration(this.state.form, 'smtp') + setForm((prevForm) => ({ ...prevForm, isLoading: true })) + + saveEmailConfiguration(form, ConfigurationsTabTypes.SMTP) .then((response) => { - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, isLoading: false }, - })) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) ToastManager.showToast({ variant: ToastVariantType.success, description: 'Saved Successfully', }) - this.props.onSaveSuccess() - if (this.props.selectSMTPFromChild) { - this.props.selectSMTPFromChild(response?.result[0]) + onSaveSuccess() + closeSMTPConfig() + if (selectSMTPFromChild) { + selectSMTPFromChild(response?.result[0]) } }) .catch((error) => { showError(error) - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, isLoading: false }, - })) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) }) } - renderWithBackdrop(body) { - return ( - -
-
-

Configure SMTP

- -
- {body} -
-
- ) - } + const renderForm = () => ( +
+ + + - onSaveClickHandler(event) { - event.preventDefault() - this.saveSMTPConfig() - } + +
+ +
+ + +
+ ) - render() { - let body - if (this.state.view === ViewType.LOADING) { - body = ( -
- -
- ) - } else { - body = ( - <> -
- - - -
- -
-
- -
- -
-
- - Set as default configuration to send emails - -
- - -
-
- - ) - } - return this.renderWithBackdrop(body) - } + return ( + + ) } diff --git a/src/components/notifications/SMTPConfigurationTable.tsx b/src/components/notifications/SMTPConfigurationTable.tsx index 3b6ceaf0c1..40c15de0ac 100644 --- a/src/components/notifications/SMTPConfigurationTable.tsx +++ b/src/components/notifications/SMTPConfigurationTable.tsx @@ -1,98 +1,73 @@ -import { Trash } from '@Components/common' import { DeleteComponentsName } from '@Config/constantMessaging' -import { ViewType } from '@Config/constants' -import { Progressing } from '@devtron-labs/devtron-fe-common-lib' -import Tippy from '@tippyjs/react' -import { ReactComponent as Edit } from '@Icons/ic-edit.svg' +import { noop, useSearchString } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' import { ConfigurationTableProps } from './types' import { ConfigurationsTabTypes } from './constants' import { EmptyConfigurationView } from './EmptyConfigurationView' +import emptySmtp from '../../assets/img/smtp-empty.png' +import { getConfigTabIcons, renderDefaultTag, renderText } from './notifications.util' +import { ConfigTableRowActionButton } from './ConfigTableRowActionButton' -export const SMTPConfigurationTable = ({ setState, state, deleteClickHandler }: ConfigurationTableProps) => { - const { smtpConfigurationList, view } = state - if (view === ViewType.LOADING) { - return ( -
- -
- ) +export const SMTPConfigurationTable = ({ state, deleteClickHandler }: ConfigurationTableProps) => { + const { smtpConfigurationList } = state + const { searchParams } = useSearchString() + const history = useHistory() + + const onClickEditRow = (configId) => () => { + const newParams = { + ...searchParams, + configId: configId.toString(), + modal: ConfigurationsTabTypes.SMTP, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) } + if (smtpConfigurationList.length === 0) { - return + return } + return ( - - - - - - - - - - - - {smtpConfigurationList.map((smtpConfig) => ( - - ))} - - -
NameHostPortSender' Email -
+
+

+

Name

+

Host

+

Port

+

Sender' Email

+

+

+
+ {smtpConfigurationList.map((smtpConfig) => ( +
+ {getConfigTabIcons(ConfigurationsTabTypes.SMTP)} +
-
- {smtpConfig.name} - {smtpConfig.isDefault ? ( - Default - ) : null} -
-
- {smtpConfig.host} -
-
{smtpConfig.port}
-
{smtpConfig.email}
-
- - - - - - -
-
+ {renderText(smtpConfig.name, true, onClickEditRow(smtpConfig.id))} + {renderDefaultTag(smtpConfig.isDefault)} +
+ {renderText(smtpConfig.host, false, noop, `smtp-config-host-${smtpConfig.host}`)} + {renderText(smtpConfig.port)} + {renderText(smtpConfig.email)} + +
+ ))} +
+
) } diff --git a/src/components/notifications/SlackConfigModal.tsx b/src/components/notifications/SlackConfigModal.tsx index e229ddde11..5004fa8a94 100644 --- a/src/components/notifications/SlackConfigModal.tsx +++ b/src/components/notifications/SlackConfigModal.tsx @@ -1,353 +1,248 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed 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 React, { Component } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { showError, - Progressing, getTeamListMin as getProjectListMin, - Drawer, CustomInput, ToastManager, ToastVariantType, + SelectPicker, + ComponentSizeType, + OptionType, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' -import { ReactComponent as Close } from '../../assets/icons/ic-close.svg' -import { Select } from '../common' -import { ViewType } from '../../config/constants' +import { useHistory } from 'react-router-dom' import { saveSlackConfiguration, updateSlackConfiguration, getSlackConfiguration } from './notifications.service' import { ReactComponent as ICHelpOutline } from '../../assets/icons/ic-help-outline.svg' -import { ReactComponent as Error } from '../../assets/icons/ic-warning.svg' -import { REQUIRED_FIELD_MSG } from '../../config/constantMessaging' - -export interface SlackConfigModalProps { - slackConfigId: number - onSaveSuccess: () => void - closeSlackConfigModal: (event) => void -} - -export interface SlackConfigModalState { - view: string - projectList: Array<{ id: number; name: string; active: boolean }> - form: { - projectId: number - configName: string - webhookUrl: string - isLoading: boolean - isError: boolean - } - isValid: { - projectId: boolean - configName: boolean - webhookUrl: boolean - } -} - -export class SlackConfigModal extends Component { - constructor(props) { - super(props) - this.state = { - view: ViewType.LOADING, - projectList: [], - form: { - projectId: 0, - configName: '', - webhookUrl: '', - isLoading: false, - isError: false, - }, - isValid: { - projectId: true, - configName: true, - webhookUrl: true, - }, - } - this.handleSlackChannelChange = this.handleSlackChannelChange.bind(this) - this.handleWebhookUrlChange = this.handleWebhookUrlChange.bind(this) - this.handleProjectChange = this.handleProjectChange.bind(this) - this.isValid = this.isValid.bind(this) - this.onSaveClickHandler = this.onSaveClickHandler.bind(this) +import { ProjectListTypes, SlackConfigModalProps, SlackFormType } from './types' +import { ConfigurationFieldKeys, ConfigurationsTabTypes, DefaultSlackKeys, DefaultSlackValidations } from './constants' +import { ConfigurationTabDrawerModal } from './ConfigurationDrawerModal' +import { renderErrorToast, validateKeyValueConfig } from './notifications.util' + +export const SlackConfigModal: React.FC = ({ + slackConfigId, + onSaveSuccess, + closeSlackConfigModal, +}: SlackConfigModalProps) => { + const history = useHistory() + const projectRef = useRef(null) + + const [projectList, setProjectList] = useState([]) + const [selectedProject, setSelectedProject] = useState() + const [form, setForm] = useState(DefaultSlackKeys) + const [isFormValid, setFormValid] = useState(DefaultSlackValidations) + + const fetchSlackConfig = async () => { + setForm((prevForm) => ({ ...prevForm, isLoading: true })) + Promise.all([getSlackConfiguration(slackConfigId), getProjectListMin()]) + .then(([slackConfigRes, projectListRes]) => { + setProjectList(projectListRes.result || []) + setForm({ ...slackConfigRes.result, isLoading: false }) + setFormValid(DefaultSlackValidations) + setSelectedProject({ + label: projectListRes.result.find((p) => p.id === slackConfigRes.result.projectId).name, + value: slackConfigRes.result.projectId, + }) + }) + .catch((error) => { + showError(error) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) + }) } - componentDidMount() { - if (this.props.slackConfigId) { - Promise.all([getSlackConfiguration(this.props.slackConfigId), getProjectListMin()]) - .then(([slackConfigRes, projectListRes]) => { - const state = { ...this.state } - state.view = ViewType.FORM - state.projectList = projectListRes.result || [] - state.form = { ...slackConfigRes.result } - state.isValid = { - projectId: true, - configName: true, - webhookUrl: true, - } - this.setState(state) - }) - .catch((error) => { - showError(error) - }) + useEffect(() => { + if (slackConfigId) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchSlackConfig() } else { getProjectListMin() .then((response) => { - this.setState({ - projectList: response.result || [], - view: ViewType.FORM, - }) + setProjectList(response.result || []) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) }) .catch((error) => { showError(error) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) }) } - } - - handleSlackChannelChange(event: React.ChangeEvent): void { - const { form } = { ...this.state } - form.configName = event.target.value - this.setState({ form }) - } + }, [slackConfigId]) - isValid(event, key: 'configName' | 'webhookUrl' | 'projectId'): void { - const { form, isValid } = { ...this.state } - if (key === 'projectId') { - isValid[key] = event.target.value + const closeSlackConfig = () => { + if (typeof closeSlackConfigModal === 'function') { + closeSlackConfigModal() } else { - isValid[key] = event.target.value.length !== 0 + const newParams = { + modal: ConfigurationsTabTypes.SLACK, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) } - this.setState({ form, isValid }) - } - - handleWebhookUrlChange(event: React.ChangeEvent): void { - const { form } = { ...this.state } - form.webhookUrl = event.target.value - this.setState({ form }) } - handleProjectChange(event: React.ChangeEvent): void { - const { form, isValid } = { ...this.state } - form.projectId = Number(event.target.value) - isValid.projectId = !!event.target.value - this.setState({ form, isValid }) + const getAllFieldsValidated = (): boolean => { + const { configName, webhookUrl } = form + return !!configName && !!webhookUrl && !!selectedProject?.value } - saveSlackConfig(): void { - const state = { ...this.state } - state.form.isLoading = true - state.isValid.projectId = !!state.form.projectId - this.setState(state) - const keys = Object.keys(this.state.isValid) - const isFormValid = keys.reduce((isFormValid, key) => { - isFormValid = isFormValid && this.state.isValid[key] - return isFormValid - }, true) - - if (!isFormValid) { - state.form.isLoading = false - state.form.isError = true - this.setState(state) + const saveSlackConfig = () => { + if (!getAllFieldsValidated()) { + setForm((prevForm) => ({ ...prevForm, isLoading: false })) + setFormValid((prevValid) => ({ + ...prevValid, + configName: validateKeyValueConfig(ConfigurationFieldKeys.CONFIG_NAME, form.configName), + webhookUrl: validateKeyValueConfig(ConfigurationFieldKeys.WEBHOOK_URL, form.webhookUrl), + projectId: validateKeyValueConfig(ConfigurationFieldKeys.PROJECT_ID, selectedProject?.value ?? ''), + })) + renderErrorToast() return } - const requestBody = this.state.form - if (this.props.slackConfigId) { - requestBody['id'] = this.props.slackConfigId + + let requestBody = { ...form } + if (slackConfigId) { + requestBody = { + ...form, + id: slackConfigId, + } } - const promise = this.props.slackConfigId - ? updateSlackConfiguration(requestBody) - : saveSlackConfiguration(requestBody) + setForm((prevForm) => ({ ...prevForm, isLoading: true })) + + const promise = slackConfigId ? updateSlackConfiguration(requestBody) : saveSlackConfiguration(requestBody) promise - .then((response) => { - const state = { ...this.state } - state.form.isLoading = false - state.form.isError = false - this.setState(state) + .then(() => { + setForm((prevForm) => ({ ...prevForm, isLoading: false })) ToastManager.showToast({ variant: ToastVariantType.success, description: 'Saved Successfully', }) - this.props.onSaveSuccess() + onSaveSuccess() + closeSlackConfig() }) .catch((error) => { showError(error) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) }) } - renderWithBackdrop(body) { - return ( - -
-
-

Configure Slack

- -
- {body} -
-
- ) + const renderInfoText = () => ( + + How to setup slack webhooks? + + ) + + const renderWebhookUrlLabel = () => ( +
+
Webhook URL
+ {renderInfoText()} +
+ ) + + const renderProjectLabel = () => ( +
+ Project + + + + + +
+ ) + + const handleInputChange = (event: React.ChangeEvent) => { + const { name, value } = event.target + setForm((prevForm) => ({ ...prevForm, [name]: value })) + setFormValid((prevValid) => ({ + ...prevValid, + [name]: validateKeyValueConfig(name as ConfigurationFieldKeys, value), + })) } - onSaveClickHandler(event) { - event.preventDefault() - this.saveSlackConfig() + const handleBlur = (e) => { + const { name, value } = e.target + setFormValid((prevValid) => ({ ...prevValid, [name]: validateKeyValueConfig(name, value) })) } - renderWebhookUrlLabel = () => { - return ( -
-
Webhook URL
-
- - Learn how to setup slack webhooks - - } - > -
- -
-
-
-
- ) + const handleProjectChange = (_selectedProject) => { + setSelectedProject(_selectedProject) + setForm((prevForm) => ({ + ...prevForm, + projectId: Number(_selectedProject?.value), + })) + setFormValid((prevValid) => ({ + ...prevValid, + projectId: validateKeyValueConfig(ConfigurationFieldKeys.PROJECT_ID, _selectedProject?.value ?? ''), + })) } - render() { - const project = this.state.projectList.find((p) => p.id === this.state.form.projectId) - let body - if (this.state.view === ViewType.LOADING) { - body = ( -
- -
- ) - } else { - body = ( - <> -
- - -
- - - - {!this.state.isValid.projectId ? ( - <> - - This is as required field.
- - ) : null} -
-
-
-
-
- - -
-
- - ) - } - - return this.renderWithBackdrop(body) + const handleProjectBlur = () => { + const selectedValue = selectedProject + setFormValid((prevValid) => ({ + ...prevValid, + [ConfigurationFieldKeys.PROJECT_ID]: validateKeyValueConfig( + ConfigurationFieldKeys.PROJECT_ID, + selectedValue?.value ?? '', + ), + })) } + + const renderContent = () => ( +
+ + + ({ label: p.name, value: p.id.toString() }))} + size={ComponentSizeType.large} + error={isFormValid[ConfigurationFieldKeys.PROJECT_ID].message} + onBlur={handleProjectBlur} + selectRef={projectRef} + /> +
+ ) + + return ( + + ) } diff --git a/src/components/notifications/SlackConfigurationTable.tsx b/src/components/notifications/SlackConfigurationTable.tsx index 67cc74a8c3..b8c01890f8 100644 --- a/src/components/notifications/SlackConfigurationTable.tsx +++ b/src/components/notifications/SlackConfigurationTable.tsx @@ -1,84 +1,67 @@ -import { Trash } from '@Components/common' import { DeleteComponentsName } from '@Config/constantMessaging' -import { ViewType } from '@Config/constants' -import { Progressing } from '@devtron-labs/devtron-fe-common-lib' -import Tippy from '@tippyjs/react' -import { ReactComponent as Edit } from '@Icons/ic-edit.svg' +import { useSearchString } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' import { ConfigurationTableProps } from './types' import { EmptyConfigurationView } from './EmptyConfigurationView' import { ConfigurationsTabTypes } from './constants' +import { getConfigTabIcons, renderText } from './notifications.util' +import './notifications.scss' +import emptySlack from '../../assets/img/slack-empty.png' +import { ConfigTableRowActionButton } from './ConfigTableRowActionButton' + +const SlackConfigurationTable = ({ state, deleteClickHandler }: ConfigurationTableProps) => { + const { searchParams } = useSearchString() + const history = useHistory() + const { slackConfigurationList } = state -export const SlackConfigurationTable = ({ setState, state, deleteClickHandler }: ConfigurationTableProps) => { - const { slackConfigurationList, view } = state - if (view === ViewType.LOADING) { - return ( -
- -
- ) - } if (slackConfigurationList.length === 0) { - return + return + } + + const onClickSlackConfigEdit = (id: number) => () => { + const newParams = { + ...searchParams, + configId: id.toString(), + modal: ConfigurationsTabTypes.SLACK, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) } + return ( - - - - - - - - - - {slackConfigurationList.map((slackConfig) => ( - - ))} - - -
NameWebhook URL -
-
- {slackConfig.slackChannel} -
-
- {slackConfig.webhookUrl} -
-
- - - - - - -
-
+
+
+
+

Name

+

Webhook URL

+

+

+
+ {slackConfigurationList.map((slackConfig) => ( +
+ {getConfigTabIcons(ConfigurationsTabTypes.SLACK)} +
+ {renderText(slackConfig.slackChannel, true, onClickSlackConfigEdit(slackConfig.id))} +
+ {renderText(slackConfig.webhookUrl)} + +
+ ))} +
+
) } + +export default SlackConfigurationTable diff --git a/src/components/notifications/WebhookConfigDynamicDataTable.tsx b/src/components/notifications/WebhookConfigDynamicDataTable.tsx new file mode 100644 index 0000000000..caf1b3edae --- /dev/null +++ b/src/components/notifications/WebhookConfigDynamicDataTable.tsx @@ -0,0 +1,78 @@ +import { DynamicDataTable } from '@devtron-labs/devtron-fe-common-lib' +import { VariableDataTableActionType } from '@Components/CIPipelineN/VariableDataTable/types' +import { useEffect } from 'react' +import { getEmptyVariableDataRow, getInitialWebhookKeyRow, getTableHeaders } from './notifications.util' +import { + HandleRowUpdateActionProps, + WebhookConfigDynamicDataTableProps, + WebhookDataRowType, + WebhookHeaderKeyType, +} from './types' + +export const WebhookConfigDynamicDataTable = ({ rows, setRows, headers }: WebhookConfigDynamicDataTableProps) => { + useEffect(() => { + setRows(getInitialWebhookKeyRow(headers)) + }, []) + + const handleRowUpdateAction = ({ actionType, actionValue, rowId, headerKey }: HandleRowUpdateActionProps) => { + let updatedRows: WebhookDataRowType[] = [...rows] + switch (actionType) { + case VariableDataTableActionType.UPDATE_ROW: + updatedRows = rows.map((row) => + row.id === rowId + ? { + ...row, + data: { + ...row.data, + [headerKey]: { + ...row.data[headerKey], + value: actionValue, + }, + }, + } + : row, + ) + break + + default: + break + } + + setRows(updatedRows) + } + + const dataTableHandleChange = (updatedRow: WebhookDataRowType, headerKey: WebhookHeaderKeyType, value: string) => { + if (!updatedRow || !updatedRow.id) return + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: value, + headerKey, + rowId: updatedRow.id, + }) + } + + const onClickAddRow = () => { + const newRow = getEmptyVariableDataRow() + setRows([newRow, ...rows]) + } + + const onDeleteRow = (row: WebhookDataRowType) => { + const remainingRows = rows.filter(({ id }) => id !== row?.id) + if (remainingRows.length === 0) { + const emptyRowData = getEmptyVariableDataRow() + setRows([emptyRowData]) + return + } + setRows(remainingRows) + } + + return ( + + ) +} diff --git a/src/components/notifications/WebhookConfigModal.tsx b/src/components/notifications/WebhookConfigModal.tsx index e315c06b14..9f64f5afdd 100644 --- a/src/components/notifications/WebhookConfigModal.tsx +++ b/src/components/notifications/WebhookConfigModal.tsx @@ -13,419 +13,255 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import React, { Component } from 'react' +import { useState, useEffect } from 'react' import { showError, - Progressing, - getTeamListMin as getProjectListMin, - Drawer, CustomInput, ClipboardButton, CodeEditor, ToastVariantType, ToastManager, } from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as Close } from '../../assets/icons/ic-close.svg' -import { ViewType } from '../../config/constants' -import { getWebhookAttributes, getWebhookConfiguration, saveUpdateWebhookConfiguration } from './notifications.service' -import { ReactComponent as Error } from '../../assets/icons/ic-warning.svg' -import { ReactComponent as Add } from '../../assets/icons/ic-add.svg' +import { useHistory } from 'react-router-dom' +import { ReactComponent as ErrorIcon } from '@Icons/ic-warning.svg' import { ReactComponent as Help } from '../../assets/icons/ic-help.svg' -import { WebhhookConfigModalState, WebhookConfigModalProps } from './types' -import CreateHeaderDetails from './CreateHeaderDetails' -import { REQUIRED_FIELD_MSG } from '../../config/constantMessaging' +import { getWebhookAttributes, getWebhookConfiguration, saveUpdateWebhookConfiguration } from './notifications.service' +import { + ConfigurationFieldKeys, + ConfigurationsTabTypes, + DefaultWebhookConfig, + DefaultWebhookValidations, +} from './constants' +import { ConfigurationTabDrawerModal } from './ConfigurationDrawerModal' +import { WebhookConfigModalProps, WebhookDataRowType, WebhookFormTypes, WebhookValidations } from './types' +import { WebhookConfigDynamicDataTable } from './WebhookConfigDynamicDataTable' +import { renderErrorToast, validateKeyValueConfig, validatePayloadField } from './notifications.util' -export class WebhookConfigModal extends Component { - constructor(props) { - super(props) - this.state = { - view: ViewType.LOADING, - form: { - configName: '', - webhookUrl: '', +export const WebhookConfigModal = ({ + webhookConfigId, + closeWebhookConfigModal, + onSaveSuccess, +}: WebhookConfigModalProps) => { + const [form, setForm] = useState(DefaultWebhookConfig) + const [rows, setRows] = useState() + const [isFormValid, setFormValid] = useState(DefaultWebhookValidations) + + const history = useHistory() + const [webhookAttribute, setWebhookAttribute] = useState({}) + + const fetchWebhookData = async () => { + setForm((prev) => ({ ...prev, isLoading: true })) + try { + // Fetch webhook configuration + const response = await getWebhookConfiguration(webhookConfigId) + const { header = {}, payload = '' } = response?.result || {} + const headers = Object.entries(header || {}).map(([key, value]) => ({ + key, + value, + })) + // Update form state with response data + setForm((prev) => ({ + ...prev, + ...response?.result, + header: headers, + payload, isLoading: false, - isError: false, - payload: '', - header: [{ key: '', value: '' }], - }, - isValid: { - configName: true, - webhookUrl: true, - payload: true, - }, - webhookAttribute: {}, - copyAttribute: false, + })) + } catch (error) { + // Show error message and reset loading state + showError(error) + setForm((prev) => ({ ...prev, isLoading: false })) } - this.handleWebhookConfigNameChange = this.handleWebhookConfigNameChange.bind(this) - this.handleWebhookUrlChange = this.handleWebhookUrlChange.bind(this) - this.handleWebhookPaylodChange = this.handleWebhookPaylodChange.bind(this) - this.addNewHeader = this.addNewHeader.bind(this) - this.renderHeadersList = this.renderHeadersList.bind(this) - this.setHeaderData = this.setHeaderData.bind(this) - this.removeHeader = this.removeHeader.bind(this) - this.renderHeadersList = this.renderHeadersList.bind(this) - this.setCopied = this.setCopied.bind(this) - this.isValid = this.isValid.bind(this) - this.onSaveClickHandler = this.onSaveClickHandler.bind(this) - this.onClickSave = this.onClickSave.bind(this) - this.onBlur = this.onBlur.bind(this) } - componentDidMount() { - if (this.props.webhookConfigId) { - getWebhookConfiguration(this.props.webhookConfigId) - .then((response) => { - const state = { ...this.state } - const _headers = [...this.state.form.header] - state.view = ViewType.FORM - const _responseKeys = response.result?.header ? Object.keys(response.result.header) : [] - _responseKeys.forEach((_key) => { - _headers.push({ key: _key, value: response.result.header[_key] }) - }) - const _responsePayload = response.result?.payload ?? '' - state.form = { - ...response.result, - header: _headers, - payload: _responsePayload, - } - state.isValid = { - configName: true, - webhookUrl: true, - payload: true, - } - this.setState(state) - }) - .catch((error) => { - showError(error) - }) - } else { - getProjectListMin() - .then((response) => { - this.setState({ - view: ViewType.FORM, - }) - }) - .catch((error) => { - showError(error) - }) + const fetchAttribute = async () => { + setForm((prev) => ({ ...prev, isLoading: true })) + try { + // Fetch webhook attributes + const attributesResponse = await getWebhookAttributes() + setWebhookAttribute(attributesResponse?.result || {}) + setForm((prev) => ({ ...prev, isLoading: false })) + } catch (error) { + showError(error) + setForm((prev) => ({ ...prev, isLoading: false })) } - getWebhookAttributes() - .then((response) => { - const state = { ...this.state } - state.webhookAttribute = { ...response.result } - this.setState(state) - }) - .catch((error) => { - showError(error) - }) } - handleWebhookConfigNameChange(event: React.ChangeEvent): void { - const { form } = { ...this.state } - form.configName = event.target.value - this.setState({ form }) - } + useEffect(() => { + // Fetch webhook attributes + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchAttribute() + }, []) - isValid(event, key: 'configName' | 'webhookUrl' | 'payload'): void { - const { form, isValid } = { ...this.state } - if (key != 'payload') { - isValid[key] = event.target.value.length !== 0 - } else if (this.state.form.payload != '') { - try { - isValid[key] = event.target.value.length !== 0 - if (isValid[key]) { - isValid[key] = true - } - } catch (err) { - isValid[key] = false - } + useEffect(() => { + if (webhookConfigId) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchWebhookData() } - this.setState({ form, isValid }) - } - - handleWebhookUrlChange(event: React.ChangeEvent): void { - const { form } = { ...this.state } - form.webhookUrl = event.target.value - this.setState({ form }) - } - - handleWebhookPaylodChange(value): void { - const { form } = { ...this.state } - form.payload = value - this.setState({ form }) - } + }, [webhookConfigId]) - saveWebhookConfig(): void { - const state = { ...this.state } - state.form.isLoading = true - this.setState(state) - const keys = Object.keys(this.state.isValid) - const isFormValid = keys.reduce((isFormValid, key) => { - isFormValid = isFormValid && this.state.isValid[key] - return isFormValid - }, true) - if (!isFormValid) { - state.form.isLoading = false - state.form.isError = true - this.setState(state) - return - } - const requestBody = this.state.form - if (this.props.webhookConfigId) { - requestBody['id'] = this.props.webhookConfigId + const closeWebhookConfig = () => { + if (typeof closeWebhookConfigModal === 'function') { + closeWebhookConfigModal() } else { - requestBody['id'] = 0 - } - saveUpdateWebhookConfiguration(requestBody) - .then((response) => { - const state = { ...this.state } - state.form.isLoading = false - state.form.isError = false - this.setState(state) - ToastManager.showToast({ - variant: ToastVariantType.success, - description: 'Saved Successfully', - }) - this.props.onSaveSuccess() - }) - .catch((error) => { - showError(error) + const newParams = { + modal: ConfigurationsTabTypes.WEBHOOK, + } + history.push({ + search: new URLSearchParams(newParams).toString(), }) + } } - setHeaderData(index, _headerData) { - const _headers = [...this.state.form.header] - _headers[index] = _headerData - const { form } = { ...this.state } - form.header = _headers - this.setState({ form }) - } - - addNewHeader() { - const _headers = [...this.state.form.header] - _headers.splice(0, 0, { - key: '', - value: '', - }) - const { form } = { ...this.state } - form.header = _headers - this.setState({ form }) - } - - removeHeader(index) { - const _headers = [...this.state.form.header] - _headers.splice(index, 1) - const { form } = { ...this.state } - form.header = _headers - this.setState({ form }) + const handleInputChange = (event) => { + const { name, value } = event.target + setForm((prev) => ({ ...prev, [name]: value })) + setFormValid((prev) => ({ ...prev, [name]: validateKeyValueConfig(name, value) })) } - setCopied(value: boolean) { - this.setState({ copyAttribute: value }) + const handlePayloadChange = (value) => { + setForm((prev) => ({ ...prev, payload: value })) + setFormValid((prev) => ({ ...prev, payload: validatePayloadField(value) })) } - renderDataList(attribute, index) { - return ( -
- {this.state.webhookAttribute[attribute]} -
- + const renderDataList = () => ( +
+ {Object.keys(webhookAttribute).map((attribute, index) => ( +
+

{webhookAttribute[attribute]}

+
+ +
-
- ) - } + ))} +
+ ) - renderConfigureLinkInfoColumn() { - const keys = Object.keys(this.state.webhookAttribute) - return ( -
-
- - Available data -
- - Following data are available to be shared through Webhook. Use Payload to configure. - - {keys.map((attribute, index) => this.renderDataList(attribute, index))} + const renderConfigureLinkInfoColumn = () => ( +
+
+ + Available data
- ) - } +

+ Following data are available to be shared through Webhook. Use Payload to configure. +

+ {renderDataList()} +
+ ) - renderHeadersList() { - return ( -
- {this.state.form.header?.map((headerData, index) => ( - - ))} -
- ) + const getAllFieldsValidated = (): boolean => { + const { configName, webhookUrl, payload } = form + return !!configName && !!webhookUrl && validatePayloadField(payload).isValid } - renderWithBackdrop(body) { - return ( - -
-
-

Configure Webhook

- -
- {body} -
-
- ) - } + const saveWebhookConfig = async () => { + if (!getAllFieldsValidated()) { + setFormValid({ + ...isFormValid, + configName: validateKeyValueConfig(ConfigurationFieldKeys.CONFIG_NAME, form.configName), + webhookUrl: validateKeyValueConfig(ConfigurationFieldKeys.WEBHOOK_URL, form.webhookUrl), + payload: validatePayloadField(form.payload), + }) + renderErrorToast() + } - onSaveClickHandler(event) { - event.preventDefault() - this.saveWebhookConfig() - } + if (!getAllFieldsValidated()) { + setForm((prev) => ({ ...prev, isLoading: false })) + setFormValid({ + ...isFormValid, + configName: validateKeyValueConfig(ConfigurationFieldKeys.CONFIG_NAME, form.configName), + webhookUrl: validateKeyValueConfig(ConfigurationFieldKeys.WEBHOOK_URL, form.webhookUrl), + payload: validatePayloadField(form.payload), + }) + return + } - onClickSave(event) { - event.preventDefault() - this.saveWebhookConfig() + try { + const requestBody = { + ...form, + id: webhookConfigId || 0, + } + await saveUpdateWebhookConfiguration(requestBody) + ToastManager.showToast({ variant: ToastVariantType.success, description: 'Saved Successfully' }) + onSaveSuccess() + closeWebhookConfig() + } catch (error) { + showError(error) + } finally { + setForm((prev) => ({ ...prev, isLoading: false })) + } } - onBlur(event) { - this.isValid(event, event.currentTarget.dataset.field) + const handleBlur = (event) => { + const { name, value } = event.target + setFormValid((prev) => ({ ...prev, [name]: validateKeyValueConfig(name, value) })) } + const renderWebhookModal = () => ( +
+
+ + + - renderWebhookModal = () => { - if (this.state.view === ViewType.LOADING) { - return ( -
- -
- ) - } - return ( - <> -
-
- - -
-
- Headers - - Add - -
- {this.renderHeadersList()} -
- -
- {this.renderConfigureLinkInfoColumn()} -
-
-
- - +
+
Data to be shared through webhook
+
+
+ {isFormValid[ConfigurationFieldKeys.PAYLOAD].message && ( +
+ + {isFormValid[ConfigurationFieldKeys.PAYLOAD].message} +
+ )}
- - ) - } +
+ {renderConfigureLinkInfoColumn()} +
+ ) - render() { - return this.renderWithBackdrop(this.renderWebhookModal()) - } + return ( + + ) } diff --git a/src/components/notifications/WebhookConfigurationTable.tsx b/src/components/notifications/WebhookConfigurationTable.tsx index 7a7ad7a595..26ce598ba9 100644 --- a/src/components/notifications/WebhookConfigurationTable.tsx +++ b/src/components/notifications/WebhookConfigurationTable.tsx @@ -1,94 +1,68 @@ -import { Trash } from '@Components/common' import { DeleteComponentsName } from '@Config/constantMessaging' -import { ViewType } from '@Config/constants' -import { Progressing } from '@devtron-labs/devtron-fe-common-lib' -import Tippy from '@tippyjs/react' -import { ReactComponent as Edit } from '@Icons/ic-edit.svg' +import { noop, useSearchString } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' import { ConfigurationTableProps } from './types' import { EmptyConfigurationView } from './EmptyConfigurationView' import { ConfigurationsTabTypes } from './constants' +import { ConfigTableRowActionButton } from './ConfigTableRowActionButton' +import { getConfigTabIcons, renderText } from './notifications.util' +import webhookEmpty from '../../assets/img/webhook-empty.png' + +export const WebhookConfigurationTable = ({ state, deleteClickHandler }: ConfigurationTableProps) => { + const { webhookConfigurationList } = state + const { searchParams } = useSearchString() + const history = useHistory() -export const WebhookConfigurationTable = ({ setState, state, deleteClickHandler }: ConfigurationTableProps) => { - const { view, webhookConfigurationList } = state - if (view === ViewType.LOADING) { - return ( -
- -
- ) - } if (webhookConfigurationList.length === 0) { - return + return } - const editWebhookHandler = (e) => { - setState({ ...state, showWebhookConfigModal: true, webhookConfigId: e.currentTarget.dataset.webhookid }) + const onClickWebhookConfigEdit = (id: number) => () => { + const newParams = { + ...searchParams, + configId: id.toString(), + modal: ConfigurationsTabTypes.WEBHOOK, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) } return ( - - - - - - - - - - {webhookConfigurationList.map((webhookConfig) => ( - - ))} - - -
NameWebhook URL -
-
- {webhookConfig.name} -
-
- {webhookConfig.webhookUrl} -
-
- - - - - - -
-
+
+
+

+

Name

+

Webhook URL

+

+

+
+ {webhookConfigurationList.map((webhookConfig) => ( +
+ {getConfigTabIcons(ConfigurationsTabTypes.WEBHOOK)} + {renderText( + webhookConfig.name, + true, + onClickWebhookConfigEdit(webhookConfig.id), + `webhook-config-name-${webhookConfig.name}`, + )} + {renderText(webhookConfig.name, false, noop, `webhook-url-${webhookConfig.webhookUrl}`)} + +
+ ))} +
+
) } diff --git a/src/components/notifications/constants.ts b/src/components/notifications/constants.ts index f2080c492c..f5b1481daf 100644 --- a/src/components/notifications/constants.ts +++ b/src/components/notifications/constants.ts @@ -1,3 +1,7 @@ +// ------------ Configuration Constants ------------ + +import { SlackFormType } from './types' + export enum ConfigurationsTabTypes { SES = 'ses', SMTP = 'smtp', @@ -5,6 +9,13 @@ export enum ConfigurationsTabTypes { WEBHOOK = 'webhook', } +export const ConfigurationTabText = { + SES: 'Email (SES)', + SMTP: 'Email (SMTP)', + SLACK: 'Slack', + WEBHOOK: 'Webhook', +} + export const EmptyConfigurationSubTitle = { [ConfigurationsTabTypes.SES]: 'SES configuration will be used to send email notifications to the desired email address', @@ -14,9 +25,84 @@ export const EmptyConfigurationSubTitle = { [ConfigurationsTabTypes.WEBHOOK]: 'Configure webhook to send event data to external tools', } -export const ConfigurationTabText = { - SES: 'Email (SES)', - SMTP: 'Email (SMTP)', - SLACK: 'Slack', - WEBHOOK: 'Webhook', +export enum ConfigurationFieldKeys { + CONFIG_NAME = 'configName', + ACCESS_KEY = 'accessKey', + SECRET_KEY = 'secretKey', + REGION = 'region', + FROM_EMAIL = 'fromEmail', + PROJECT_ID = 'projectId', + WEBHOOK_URL = 'webhookUrl', + CONFIG_ID = 'configId', + HOST = 'host', + PORT = 'port', + AUTH_USER = 'authUser', + AUTH_PASSWORD = 'authPassword', + DEFAULT = 'default', + PAYLOAD = 'payload', +} + +// ------------ SES Configuration Constants ------------ + +export const ConfigValidationKeys = { isValid: true, message: '' } + +export const DefaultSESValidations = { + [ConfigurationFieldKeys.CONFIG_NAME]: ConfigValidationKeys, + [ConfigurationFieldKeys.ACCESS_KEY]: ConfigValidationKeys, + [ConfigurationFieldKeys.SECRET_KEY]: ConfigValidationKeys, + [ConfigurationFieldKeys.REGION]: ConfigValidationKeys, + [ConfigurationFieldKeys.FROM_EMAIL]: ConfigValidationKeys, } + +export const SESSortableHeaderTitle = { + CONFIG_NAME: 'Name', + ACCESS_KEY: 'Access Key Id', + EMAIL: "Sender's Email", +} + +// ------------ SMTP Configuration Constants ------------ + +export const DefaultSMTPValidation = { + [ConfigurationFieldKeys.CONFIG_NAME]: ConfigValidationKeys, + [ConfigurationFieldKeys.HOST]: ConfigValidationKeys, + [ConfigurationFieldKeys.PORT]: ConfigValidationKeys, + [ConfigurationFieldKeys.AUTH_USER]: ConfigValidationKeys, + [ConfigurationFieldKeys.AUTH_PASSWORD]: ConfigValidationKeys, + [ConfigurationFieldKeys.FROM_EMAIL]: ConfigValidationKeys, +} + +// ------------ Slack Configuration Constants ------------ + +export const DefaultSlackKeys: SlackFormType = { + [ConfigurationFieldKeys.PROJECT_ID]: 0, + [ConfigurationFieldKeys.CONFIG_NAME]: '', + [ConfigurationFieldKeys.WEBHOOK_URL]: '', + isLoading: false, + id: null, +} + +export const DefaultSlackValidations = { + [ConfigurationFieldKeys.PROJECT_ID]: ConfigValidationKeys, + [ConfigurationFieldKeys.CONFIG_NAME]: ConfigValidationKeys, + [ConfigurationFieldKeys.WEBHOOK_URL]: ConfigValidationKeys, +} + +// ------------ Webhook Configuration Constants ------------ + +export const DefaultHeaders = { key: '', value: '' } + +export const DefaultWebhookConfig = { + [ConfigurationFieldKeys.CONFIG_NAME]: '', + [ConfigurationFieldKeys.WEBHOOK_URL]: '', + [ConfigurationFieldKeys.PAYLOAD]: '', + isLoading: false, + header: [DefaultHeaders], +} + +export const DefaultWebhookValidations = { + [ConfigurationFieldKeys.CONFIG_NAME]: ConfigValidationKeys, + [ConfigurationFieldKeys.WEBHOOK_URL]: ConfigValidationKeys, + [ConfigurationFieldKeys.PAYLOAD]: ConfigValidationKeys, +} + +export const SlackIncomingWebhookUrl = 'https://slack.com/marketplace/A0F7XDUAZ-incoming-webhooks' diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 4e89fd7031..2c1fb07120 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -122,17 +122,6 @@ box-shadow: 0 -1px 0 0 var(--N200); } -.configuration-tab__header { - display: flex; - justify-content: space-between; - padding: 18px 24px; -} - -.configuration-tab__title { - font-size: 14px; - font-weight: 600; - margin: 0px; -} .white-card--configuration-tab { overflow-y: auto; @@ -141,92 +130,71 @@ height: 100%; } -.configuration-tab { - display: grid; - grid-template-rows: 1fr 1fr 1fr 1fr; - padding: 16px 24px; - grid-gap: 16px; - height: calc(100vh - 220px); -} - -.configuration-tab__table-row { - display: flex; - font-size: 14px; - font-weight: normal; - line-height: 1.43; - letter-spacing: normal; - color: var(--N600); - align-items: center; - padding: 10px 24px; - height: 48px; - cursor: default; - box-shadow: inset 0 -1px 0 0 var(--N100); - &:hover { - background-color: var(--window-bg); +.configuration-drawer{ + .webhook-config-modal{ + display: grid; + grid-template-columns: 1fr 280px; } } +// --------------Configuration Tab---------------- +.configuration-tab__container { + .configuration-tab__table-row { + display: flex; + font-size: 14px; + font-weight: normal; + line-height: 1.43; + letter-spacing: normal; + color: var(--N900); + align-items: center; + padding: 10px 20px; + height: 48px; + } -.configuration-tab__table-header { - display: flex; - font-size: 12px; - font-weight: 600; - line-height: 1.5; - letter-spacing: normal; - color: var(--N500); - text-transform: uppercase; - align-items: center; - padding: 9px 24px; - cursor: default; - box-shadow: 0 1px 0 0 var(--N200); -} - -.ses-config-table__name { - flex-basis: 25%; - color: var(--N900); - font-weight: inherit; -} - -.ses-config-table__access-key { - flex-basis: 20%; - font-weight: inherit; -} - -.ses-config-table__email { - flex-basis: 40%; - font-weight: inherit; -} + .ses-config-container { + .ses-config-grid { + display: grid; + grid-template-columns: 24px 250px 1fr 1fr 52px; + } + } -.smtp-config-table__host { - flex-basis: 33%; - font-weight: inherit; -} + .slack-config-container { + .slack-config-grid { + display: grid; + grid-template-columns: 24px 250px 1fr 52px; + } + } -.smtp-config-table__port { - flex-basis: 5%; - font-weight: inherit; -} + .smtp-config-container { + .smtp-config-grid { + display: grid; + grid-template-columns: 24px 250px 1fr 60px 1fr 52px; + } + } -.smtp-config-table__email { - flex-basis: 30%; - font-weight: inherit; -} + .webhook-config-container { + .webhook-config-grid { + display: grid; + grid-template-columns: 24px 250px 1fr 52px; + } + } -.slack-config-table__name { - flex-basis: 25%; - color: var(--N900); - font-weight: inherit; -} + .configuration-tab__table-row:hover .slack-config-table__action, + .configuration-tab__table-row:hover .ses-config-table__action, + .configuration-tab__table-row:hover .webhook-config-table__action, + .configuration-tab__table-row:hover .smtp-config-table__action { + display: flex; + } -.slack-config-table__webhook { - flex-basis: 60%; -} + .slack-config-table__action, + .ses-config-table__action, + .webhook-config-table__action, + .smtp-config-table__action { + display: none; + margin: auto; + margin-right: 0; + } -.slack-config-table__action, -.ses-config-table__action { - display: none; - margin: auto; - margin-right: 0; } .form__row--ses-account { @@ -234,12 +202,6 @@ min-width: 300px; } -.configuration-tab__table-row:hover .slack-config-table__action, -.configuration-tab__table-row:hover .ses-config-table__action { - display: flex; - background-color: var(--window-bg); -} - .kebab-menu__list--notification-tab { width: 216px; } @@ -384,7 +346,7 @@ } } -.data-conatiner { +.data-container { .hover-only { display: none; } @@ -404,12 +366,4 @@ } } } -} - -.empty-state-height { - height: 'calc(100% - 70px)'; -} - -.progressing-loader-height { - height: 'calc(100% - 68px)'; -} +} \ No newline at end of file diff --git a/src/components/notifications/notifications.util.tsx b/src/components/notifications/notifications.util.tsx index 9ebd66f7eb..e59f6b3ef0 100644 --- a/src/components/notifications/notifications.util.tsx +++ b/src/components/notifications/notifications.util.tsx @@ -15,7 +15,6 @@ */ import { components } from 'react-select' -import { validateEmail } from '../common' import { ReactComponent as ArrowDown } from '@Icons/ic-chevron-down.svg' import { ReactComponent as Email } from '@Icons/ic-mail.svg' import { ReactComponent as RedWarning } from '@Icons/ic-error-medium.svg' @@ -26,7 +25,19 @@ import { ReactComponent as Slack } from '@Icons/slack-logo.svg' import { ReactComponent as SES } from '@Icons/ic-aws-ses.svg' import { ReactComponent as Webhook } from '@Icons/ic-CIWebhook.svg' import { ReactComponent as SMTP } from '@Icons/ic-smtp.svg' -import { ConfigurationsTabTypes, ConfigurationTabText } from './constants' +import { + DynamicDataTableHeaderType, + DynamicDataTableRowDataType, + DynamicDataTableRowType, + getUniqueId, + ToastManager, + ToastVariantType, + Tooltip, +} from '@devtron-labs/devtron-fe-common-lib' +import { ConfigurationFieldKeys, ConfigurationsTabTypes, ConfigurationTabText } from './constants' +import { validateEmail } from '../common' +import { FormError, FormValidation, SESFormType, SMTPFormType, WebhookDataRowType, WebhookHeaderKeyType, WebhookRowCellType } from './types' +import { REQUIRED_FIELD_MSG } from '@Config/constantMessaging' export const multiSelectStyles = { control: (base, state) => ({ @@ -39,60 +50,48 @@ export const multiSelectStyles = { ...base, top: `38px`, }), - option: (base, state) => { - return { - ...base, - color: 'var(--N900)', - display: `flex`, - alignItems: `center`, - fontSize: '12px', - padding: '8px 24px', - } - }, - multiValue: (base, state) => { - return { - ...base, - border: - state.data.data.dest !== 'slack' && - state.data.data.dest !== 'webhook' && - !validateEmail(state.data.label) - ? `1px solid var(--R500)` - : `1px solid var(--N200)`, - borderRadius: `4px`, - background: - state.data.data.dest !== 'slack' && - state.data.data.dest !== 'webhook' && - !validateEmail(state.data.label) - ? 'var(--R100)' - : 'var(--N000)', - padding: `2px`, - textTransform: `lowercase`, - fontSize: `12px`, - lineHeight: `1.5`, - letterSpacing: `normal`, - color: `var(--N900)`, - userSelect: `none`, - display: `inline-flex`, - } - }, - multiValueLabel: (base, state) => { - return { - ...base, - display: `flex`, - alignItems: `center`, - fontSize: '12px', - padding: '0px', - } - }, + option: (base, state) => ({ + ...base, + color: 'var(--N900)', + display: `flex`, + alignItems: `center`, + fontSize: '12px', + padding: '8px 24px', + }), + multiValue: (base, state) => ({ + ...base, + border: + state.data.data.dest !== 'slack' && state.data.data.dest !== 'webhook' && !validateEmail(state.data.label) + ? `1px solid var(--R500)` + : `1px solid var(--N200)`, + borderRadius: `4px`, + background: + state.data.data.dest !== 'slack' && state.data.data.dest !== 'webhook' && !validateEmail(state.data.label) + ? 'var(--R100)' + : 'var(--N000)', + padding: `2px`, + textTransform: `lowercase`, + fontSize: `12px`, + lineHeight: `1.5`, + letterSpacing: `normal`, + color: `var(--N900)`, + userSelect: `none`, + display: `inline-flex`, + }), + multiValueLabel: (base, state) => ({ + ...base, + display: `flex`, + alignItems: `center`, + fontSize: '12px', + padding: '0px', + }), } -export const DropdownIndicator = (props) => { - return ( - - - - ) -} +export const DropdownIndicator = (props) => ( + + + +) export const MultiValueLabel = (props) => { const item = props.data @@ -164,25 +163,198 @@ export const renderPipelineTypeIcon = (row) => { return } +export const getConfigTabIcons = (tab: ConfigurationsTabTypes, size: number = 24) => { + switch (tab) { + case ConfigurationsTabTypes.SES: + return + case ConfigurationsTabTypes.SMTP: + return + case ConfigurationsTabTypes.SLACK: + return + case ConfigurationsTabTypes.WEBHOOK: + return + default: + return SES + } +} + export const getConfigurationTabTextWithIcon = () => [ { label: ConfigurationTabText.SES, - icon: SES, + icon: getConfigTabIcons(ConfigurationsTabTypes.SES, 20), link: ConfigurationsTabTypes.SES, }, { label: ConfigurationTabText.SMTP, - icon: SMTP, + icon: getConfigTabIcons(ConfigurationsTabTypes.SMTP, 20), link: ConfigurationsTabTypes.SMTP, }, { label: ConfigurationTabText.SLACK, - icon: Slack, + icon: getConfigTabIcons(ConfigurationsTabTypes.SLACK, 20), link: ConfigurationsTabTypes.SLACK, }, { label: ConfigurationTabText.WEBHOOK, - icon: Webhook, + icon: getConfigTabIcons(ConfigurationsTabTypes.WEBHOOK, 20), link: ConfigurationsTabTypes.WEBHOOK, }, ] + +export const getSESDefaultConfiguration = (shouldBeDefault: boolean): SESFormType => ({ + configName: '', + accessKey: '', + secretKey: '', + region: null, + fromEmail: '', + default: shouldBeDefault, + isLoading: false, +}) + +export const getSMTPDefaultConfiguration = (shouldBeDefault: boolean): SMTPFormType => ({ + configName: '', + host: '', + port: '', + authUser: '', + authPassword: '', + fromEmail: '', + default: shouldBeDefault, + isLoading: false, +}) + +export const renderText = (text: string, isLink: boolean = false, linkTo?: () => void, dataTestId?: string) => ( + + {isLink ? ( + + ) : ( +

+ {text || '-'} +

+ )} +
+) + +export const renderDefaultTag = (isDefault: boolean) => { + if (isDefault) { + return Default + } + return null +} + +export const getTableHeaders = (): DynamicDataTableHeaderType[] => [ + { label: 'Header key', key: 'key', width: '300px' }, + { label: 'Value', key: 'value', width: '1fr' }, +] + +export const getInitialWebhookKeyRow = (rows: WebhookRowCellType[]): DynamicDataTableRowType[] => + rows.map((row) => { + return { + data: { + key: { + value: row.key || null, + type: DynamicDataTableRowDataType.TEXT, + props: { + placeholder: 'Eg. owner-name', + }, + }, + value: { + value: row.value || '', + type: DynamicDataTableRowDataType.TEXT, + props: { + placeholder: 'Enter value', + }, + }, + }, + id: row.id, + } + }) + +export const getEmptyVariableDataRow = (): WebhookDataRowType => { + const id = getUniqueId() + return { + data: { + key: { + value: '', + type: DynamicDataTableRowDataType.TEXT, + props: { + placeholder: 'Eg. owner-name', + }, + }, + value: { + value: '', + type: DynamicDataTableRowDataType.TEXT, + props: { + placeholder: 'Enter value', + }, + }, + }, + id, + } +} + +export const validateKeyValueConfig = (key: ConfigurationFieldKeys, value: string): FormError => { + if (!value) { + return { isValid: false, message: REQUIRED_FIELD_MSG } + } + if (key === ConfigurationFieldKeys.FROM_EMAIL) { + return { isValid: validateEmail(value), message: validateEmail(value) ? '' : 'Invalid email' } + } + return { isValid: true, message: '' } +} + +export const getFormValidated = (isFormValid: FormValidation, fromEmail?: string): boolean => { + const isKeysValid = Object.values(isFormValid).every((field) => field.isValid && !field.message) + if (fromEmail) { + return isKeysValid && validateEmail(fromEmail) + } + return isKeysValid +} +export enum ConfigTableRowActionType { + ADD_ROW = 'ADD_ROW', + UPDATE_ROW = 'UPDATE_ROW', + DELETE_ROW = 'DELETE_ROW', +} + +export const getTabText = (tab: ConfigurationsTabTypes) => { + switch (tab) { + case ConfigurationsTabTypes.SES: + return 'SES' + case ConfigurationsTabTypes.SLACK: + return 'Slack' + case ConfigurationsTabTypes.WEBHOOK: + return 'Webhook' + case ConfigurationsTabTypes.SMTP: + return 'SMTP' + default: + return '' + } +} + +export const validatePayloadField = (value: string): FormError => { + let isValid = true + let errorMessage = '' + // Validate if the value is a valid JSON string + if (!value) { + return { isValid: false, message: REQUIRED_FIELD_MSG } + } + try { + JSON.parse(value) + } catch { + isValid = false + errorMessage = 'Invalid JSON format.' + } + return { isValid, message: errorMessage } +} + +export const renderErrorToast = () => + ToastManager.showToast({ + variant: ToastVariantType.error, + description: 'Some required fields are missing or Invalid', + }) diff --git a/src/components/notifications/types.tsx b/src/components/notifications/types.tsx index fa62cc55b9..2e9eb0837f 100644 --- a/src/components/notifications/types.tsx +++ b/src/components/notifications/types.tsx @@ -15,8 +15,14 @@ */ import { RouteComponentProps } from 'react-router-dom' -import { ServerError, ResponseType } from '@devtron-labs/devtron-fe-common-lib' -import { ConfigurationsTabTypes } from './constants' +import { + ServerError, + ResponseType, + DynamicDataTableRowType, + SelectPickerOptionType, +} from '@devtron-labs/devtron-fe-common-lib' +import { VariableDataTableActionType } from '@Components/CIPipelineN/VariableDataTable/types' +import { ConfigurationFieldKeys, ConfigurationsTabTypes } from './constants' export interface NotifierProps extends RouteComponentProps<{ id: string }> {} @@ -26,6 +32,7 @@ export interface NotifierState { successMessage: string | null channel: string } + export interface SMTPConfigResponseType extends ResponseType { result?: { configName: string @@ -48,41 +55,13 @@ export interface SMTPConfigModalProps { shouldBeDefault: boolean selectSMTPFromChild?: (smtpConfigId: number) => void onSaveSuccess: () => void - closeSMTPConfigModal: (event) => void + closeSMTPConfigModal?: () => void } -export interface SMTPConfigModalState { - view: string - form: { - configName: string - port: number - host: string - authUser: string - authPassword: string - fromEmail: string - default: boolean - isLoading: boolean - isError: boolean - } - isValid: { - configName: boolean - port: boolean - host: boolean - authUser: boolean - authPassword: boolean - fromEmail: boolean - } -} +// ----------------------------Configuration Tab Types---------------------------- export interface ConfigurationTabState { - view: string - showSlackConfigModal: boolean - showSESConfigModal: boolean - showSMTPConfigModal: boolean - slackConfigId: number - sesConfigId: number - smtpConfigId: number - webhookConfigId: number + isLoading: boolean sesConfigurationList: Array<{ id: number; name: string; accessKeyId: string; email: string; isDefault: boolean }> smtpConfigurationList: Array<{ id: number @@ -95,70 +74,174 @@ export interface ConfigurationTabState { slackConfigurationList: Array<{ id: number; slackChannel: string; projectId: number; webhookUrl: string }> webhookConfigurationList: Array<{ id: number; name: string; webhookUrl: string }> abortAPI: boolean - deleting: boolean confirmation: boolean sesConfig: any smtpConfig: any slackConfig: any webhookConfig: any - showDeleteConfigModalType: string - showWebhookConfigModal: boolean activeTab?: ConfigurationsTabTypes + showCannotDeleteDialogModal: boolean } export interface ConfigurationTableProps { - setState: React.Dispatch> state: ConfigurationTabState deleteClickHandler: (id: number, name: string) => void } +export interface FormError { + isValid: boolean + message: string +} + +export interface ConfigurationTabDrawerModalProps { + renderContent: () => JSX.Element + closeModal: () => void + modal: ConfigurationsTabTypes + isLoading: boolean + saveConfigModal: () => void + disableSave?: boolean +} + +export type FormValidation = { + [key: string]: FormError +} + +export interface DefaultCheckboxProps { + shouldBeDefault: boolean + handleCheckbox: () => void + isDefault: boolean +} + +// ----------------------------Configuration Tab---------------------------- + +export interface EmptyConfigurationViewProps { + configTabType: ConfigurationsTabTypes + image?: any +} + +export interface ConfigurationTabSwitcherProps { + activeTab: ConfigurationsTabTypes +} + +export interface ConfigTableRowActionButtonProps { + onClickEditRow: () => void + onClickDeleteRow: any + rootClassName: string + modal: ConfigurationsTabTypes +} + +// ----------------------------SES Config Modal---------------------------- + +export interface SESConfigModalProps { + shouldBeDefault: boolean + selectSESFromChild?: (sesConfigId: number) => void + onSaveSuccess: () => void + closeSESConfigModal?: () => void + sesConfigId: number +} + +export interface SESFormType { + configName: string + accessKey: string + secretKey: string + region: SelectPickerOptionType + default: boolean + isLoading: boolean + fromEmail: string +} + +// ----------------------------Slack Config Modal---------------------------- + +export interface ProjectListTypes { + id: number + name: string + active: boolean +} + +export interface SlackConfigModalProps { + slackConfigId: number + onSaveSuccess: () => void + closeSlackConfigModal?: () => void +} + +export interface SlackFormType { + configName: string + projectId: number + webhookUrl: string + isLoading: boolean + id: number | null +} + +// ----------------------------SMTP Config Modal---------------------------- +export interface SMTPFormType { + configName: string + port: string + host: string + authUser: string + authPassword: string + fromEmail: string + default: boolean + isLoading: boolean +} + +// ----------------------------Webhook Config Modal-------------------------------- + +export interface WebhookAttributesResponseType extends ResponseType { + result?: Record +} + export interface WebhookConfigModalProps { webhookConfigId: number + closeWebhookConfigModal?: () => void onSaveSuccess: () => void - closeWebhookConfigModal: (event) => void } -export interface WebhhookConfigModalState { - view: string - form: { - configName: string - webhookUrl: string - isLoading: boolean - isError: boolean - payload: string - header: HeaderType[] - } - isValid: { - configName: boolean - webhookUrl: boolean - payload: boolean - } - webhookAttribute: Record - copyAttribute: boolean -} +export type WebhookHeaderKeyType = 'key' | 'value' -export interface HeaderType { +export type WebhookDataRowType = DynamicDataTableRowType + +export interface WebhookRowCellType { key: string value: string + id?: number } -export interface CreateHeaderDetailsType { - index: number - headerData: HeaderType - setHeaderData: (index: number, headerData: HeaderType) => void - removeHeader?: (index: number) => void +export interface WebhookConfigDynamicDataTableProps { + rows: WebhookDataRowType[] + setRows: React.Dispatch> + headers: WebhookRowCellType[] } -export interface WebhookAttributesResponseType extends ResponseType { - result?: Record +type VariableDataTableActionPropsMap = { + [VariableDataTableActionType.UPDATE_ROW]: string } -export interface EmptyConfigurationViewProps { - configTabType: ConfigurationsTabTypes +export type VariableDataTableAction< + T extends keyof VariableDataTableActionPropsMap = keyof VariableDataTableActionPropsMap, +> = T extends keyof VariableDataTableActionPropsMap + ? { actionType: T; actionValue: VariableDataTableActionPropsMap[T] } + : never + +export type HandleRowUpdateActionProps = VariableDataTableAction & { + headerKey: WebhookHeaderKeyType + rowId: string | number } -export interface ConfigurationTabSwitcherProps { - activeTab: ConfigurationsTabTypes - state: ConfigurationTabState - setState: React.Dispatch> +export type WebhookValidations = { + [ConfigurationFieldKeys.CONFIG_NAME]: FormError + [ConfigurationFieldKeys.WEBHOOK_URL]: FormError + [ConfigurationFieldKeys.PAYLOAD]: FormError +} + +export interface HeaderType { + key: string + value: string +} + +export interface WebhookFormTypes { + configName: string + webhookUrl: string + isLoading: boolean + payload: string + header: HeaderType[] } diff --git a/src/css/base.scss b/src/css/base.scss index a1919100d4..67b3a64605 100644 --- a/src/css/base.scss +++ b/src/css/base.scss @@ -957,21 +957,6 @@ button.anchor { background-color: var(--N000); } -.dc__ses_config-table__tag { - border-radius: 2px; - padding: 2px 8px; - font-size: 11px; - font-weight: 600; - font-stretch: normal; - line-height: 1.82; - text-transform: uppercase; - margin: 0 8px; - color: var(--B500); - border: solid 1px #b5d3f2; - background-blend-mode: multiply; - background-image: linear-gradient(to bottom, var(--B50), var(--B50)); -} - .dc__saved-filter__clear-btn--dark { height: 36px; background-color: var(--white); @@ -3222,6 +3207,10 @@ textarea, .w-160 { width: 160px; + + &--imp { + width: 160px !important; + } } .w-180 { @@ -3458,6 +3447,10 @@ textarea, // width fix content .dc__w-fit-content { width: fit-content; + + &--imp { + width: fit-content !important; + } } .dc__width-inherit { @@ -3469,6 +3462,15 @@ textarea, height: inherit; } +.dc__height-auto { + height: auto; + + &--imp { + height: auto !important; + } + +} + .dc__min-width-fit-content { min-width: fit-content !important; } @@ -4383,6 +4385,10 @@ textarea::placeholder { background-color: var(--N50); } +.dc__hover-text-n90:hover { + color: var(--N900) !important; +} + .dc__bg-g5 { background-color: var(--G500) !important; } @@ -5349,9 +5355,14 @@ details[open] { background: inherit; border: none; padding: inherit; - white-space: pre-wrap; /* Since CSS 2.1 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ -} + white-space: pre-wrap; + /* Since CSS 2.1 */ + white-space: -moz-pre-wrap; + /* Mozilla, since 1999 */ + white-space: -pre-wrap; + /* Opera 4-6 */ + white-space: -o-pre-wrap; + /* Opera 7 */ + word-wrap: break-word; + /* Internet Explorer 5.5+ */ +} \ No newline at end of file