diff --git a/.eslintignore b/.eslintignore index b3aefee832..aadf4dc155 100755 --- a/.eslintignore +++ b/.eslintignore @@ -314,15 +314,9 @@ src/components/material/MaterialList.tsx src/components/material/MaterialView.tsx src/components/material/UpdateMaterial.tsx src/components/notifications/AddNotification.tsx -src/components/notifications/ConfigurationTab.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/assets/img/slack-logo.svg b/src/assets/icons/slack-logo.svg similarity index 100% rename from src/assets/img/slack-logo.svg rename to src/assets/icons/slack-logo.svg diff --git a/src/assets/img/ses-empty.png b/src/assets/img/ses-empty.png new file mode 100644 index 0000000000..7fcef34a8e Binary files /dev/null and b/src/assets/img/ses-empty.png differ diff --git a/src/assets/img/slack-empty.png b/src/assets/img/slack-empty.png new file mode 100644 index 0000000000..cb0da42874 Binary files /dev/null and b/src/assets/img/slack-empty.png differ diff --git a/src/assets/img/smtp-empty.png b/src/assets/img/smtp-empty.png new file mode 100644 index 0000000000..4eaedfda5c Binary files /dev/null and b/src/assets/img/smtp-empty.png differ diff --git a/src/assets/img/webhook-empty.png b/src/assets/img/webhook-empty.png new file mode 100644 index 0000000000..0c3d35338f Binary files /dev/null and b/src/assets/img/webhook-empty.png differ diff --git a/src/components/globalConfigurations/GlobalConfiguration.tsx b/src/components/globalConfigurations/GlobalConfiguration.tsx index 92ac48b621..faab5ad6c0 100644 --- a/src/components/globalConfigurations/GlobalConfiguration.tsx +++ b/src/components/globalConfigurations/GlobalConfiguration.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { lazy, useState, useEffect, Suspense, isValidElement } from 'react' +import { lazy, useState, useEffect, Suspense, isValidElement, useRef } from 'react' import { Route, NavLink, Router, Switch, Redirect, useHistory, useLocation } from 'react-router-dom' import { showError, @@ -852,7 +852,19 @@ export const ProtectedInput = ({ dataTestid = '', onBlur = (e) => { }, 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/AddConfigurationButton.tsx b/src/components/notifications/AddConfigurationButton.tsx new file mode 100644 index 0000000000..c8bb585897 --- /dev/null +++ b/src/components/notifications/AddConfigurationButton.tsx @@ -0,0 +1,32 @@ +import { Button, ButtonVariantType, ComponentSizeType, useSearchString } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' +import { ReactComponent as Add } from '@Icons/ic-add.svg' +import { getTabText } from './notifications.util' +import { AddConfigurationButtonProps } from './types' + +export const AddConfigurationButton = ({ activeTab }: AddConfigurationButtonProps) => { + const { searchParams } = useSearchString() + const history = useHistory() + + const handleAddClick = () => { + const newParams = { + ...searchParams, + modal: activeTab, + configId: '0', + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) + } + + return ( + - - {this.renderWebhookConfigurationTable()} - - ) - } - - renderSlackConfigurations() { - return ( -
-
-

- slack - Slack Configurations -

- -
- {this.renderSlackConfigurationTable()} -
- ) - } - - renderSlackConfigurationTable() { - if (this.state.view === ViewType.LOADING) { - return ( -
- -
- ) - } - if (this.state.slackConfigurationList.length === 0) { - return ( -
- -
- ) + } catch (error) { + showError(error, true, true) + setState({ ...state, isLoading: false }) } - return ( - - - - - - - - - - {this.state.slackConfigurationList.map((slackConfig) => { - return ( - - ) - })} - - -
NameWebhook URL -
-
- {slackConfig.slackChannel} -
-
- {slackConfig.webhookUrl} -
-
- - - - - - -
-
- ) } - editWebhookHandler(e) { - this.setState({ showWebhookConfigModal: true, webhookConfigId: e.currentTarget.dataset.webhookid }) - } + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + getAllChannelConfigs() - renderWebhookConfigurationTable() { - if (this.state.view === ViewType.LOADING) { - return ( -
- -
- ) - } - if (this.state.webhookConfigurationList.length === 0) { - return ( -
- -
- ) + const newParams = { + ...searchParams, + modal: modal ?? ConfigurationsTabTypes.SES, } - return ( - - - - - - - - - - {this.state.webhookConfigurationList.map((webhookConfig) => { - return ( - - ) - })} - - -
NameWebhook URL -
-
- {webhookConfig.name} -
-
- {webhookConfig.webhookUrl} -
-
- - - - - - -
-
- ) - } - - renderSESConfigurations() { - return ( -
-
-

- ses config - SES Configurations -

- -
- {this.renderSESConfigurationTable()} -
- ) - } - - renderSMTPConfigurations() { - return ( -
-
-

- - SMTP Configurations -

- -
- {this.renderSMTPConfigurationTable()} -
- ) - } - setDeleting = () => { - this.setState({ - deleting: true, + history.push({ + search: new URLSearchParams(newParams).toString(), }) - } + }, []) - toggleConfirmation = (confirmation) => { - this.setState({ - confirmation, - ...(!confirmation && { showDeleteConfigModalType: '' }), + const hideDeleteModal = () => { + setState({ + ...state, + confirmation: false, }) } - deleteClickHandler = async (configId, type) => { - try { - if (type === DeleteComponentsName.SlackConfigurationTab) { - const { result } = await getSlackConfiguration(configId, true) - this.setState({ - slackConfigId: configId, - slackConfig: { - ...result, - channel: DeleteComponentsName.SlackConfigurationTab, - }, - confirmation: true, - showDeleteConfigModalType: DeleteComponentsName.SlackConfigurationTab, - }) - } else if (type === DeleteComponentsName.SesConfigurationTab) { - const { result } = await getSESConfiguration(configId) - this.setState({ - sesConfigId: configId, - sesConfig: { - ...result, - channel: DeleteComponentsName.SesConfigurationTab, - }, - confirmation: true, - showDeleteConfigModalType: DeleteComponentsName.SesConfigurationTab, - }) - } else if (type === DeleteComponentsName.SMTPConfigurationTab) { - const { result } = await getSMTPConfiguration(configId) - this.setState({ - smtpConfigId: configId, - smtpConfig: { - ...result, - channel: DeleteComponentsName.SMTPConfigurationTab, - }, - confirmation: true, - showDeleteConfigModalType: DeleteComponentsName.SMTPConfigurationTab, - }) - } else if (type === DeleteComponentsName.WebhookConfigurationTab) { - const { result } = await getWebhookConfiguration(configId) - this.setState({ - webhookConfigId: configId, - webhookConfig: { - ...result, - channel: DeleteComponentsName.WebhookConfigurationTab, - }, - confirmation: true, - showDeleteConfigModalType: DeleteComponentsName.WebhookConfigurationTab, - }) - } - } catch (e) { - showError(e) + const deleteConfigPayload = (): any => { + const { activeTab, slackConfig, sesConfig, webhookConfig, smtpConfig } = state + if (activeTab === ConfigurationsTabTypes.SLACK) { + return slackConfig } - } - - renderSESConfigurationTable() { - if (this.state.view === ViewType.LOADING) { - return ( -
- -
- ) + if (activeTab === ConfigurationsTabTypes.SES) { + return sesConfig } - if (this.state.sesConfigurationList.length === 0) { - return ( -
- -
- ) + if (activeTab === ConfigurationsTabTypes.WEBHOOK) { + return webhookConfig } - return ( - - - - - - - - - - - {this.state.sesConfigurationList.map((sesConfig) => { - return ( - - ) - })} - - -
NameAccess key IdSender's Email -
-
- {sesConfig.name} - {sesConfig.isDefault ? ( - Default - ) : null} -
-
- {sesConfig.accessKeyId} -
-
{sesConfig.email}
-
- - - - - - {' '} -
-
- ) + return smtpConfig } - renderSMTPConfigurationTable() { - if (this.state.view === ViewType.LOADING) { - return ( -
- -
- ) - } - if (this.state.smtpConfigurationList.length === 0) { - return ( -
- -
- ) - } + const deletePayload = deleteConfigPayload() + + if (state.isLoading) { return ( - - - - - - - - - - - - {this.state.smtpConfigurationList.map((smtpConfig) => { - return ( - - ) - })} - - -
NameHostPortSender's Email -
-
- {smtpConfig.name} - {smtpConfig.isDefault ? ( - Default - ) : null} -
-
- {smtpConfig.host} -
-
{smtpConfig.port}
-
- {smtpConfig.email} -
-
- - - - - - -
-
+
+ +
) } - renderSESConfigModal() { - if (this.state.showSESConfigModal) { - return ( - { - this.setState({ showSESConfigModal: false, sesConfigId: 0 }) - this.getAllChannelConfigs() - }} - closeSESConfigModal={(event) => { - this.setState({ showSESConfigModal: false }) - }} - /> - ) - } - } - - renderSMTPConfigModal() { - if (this.state.showSMTPConfigModal) { - return ( - { - this.setState({ showSMTPConfigModal: false, smtpConfigId: 0 }) - this.getAllChannelConfigs() - }} - closeSMTPConfigModal={(event) => { - this.setState({ showSMTPConfigModal: false }) - }} - /> - ) + const renderEmptyState = () => { + switch (modal) { + case ConfigurationsTabTypes.SMTP: + return + case ConfigurationsTabTypes.SLACK: + return + case ConfigurationsTabTypes.WEBHOOK: + return + case ConfigurationsTabTypes.SES: + default: + return } } - renderSlackConfigModal() { - if (this.state.showSlackConfigModal) { - return ( - { - this.setState({ showSlackConfigModal: false, slackConfigId: 0 }) - this.getAllChannelConfigs() - }} - closeSlackConfigModal={(event) => { - this.setState({ showSlackConfigModal: false, slackConfigId: 0 }) - }} - /> - ) - } - } - - onSaveWebhook() { - this.setState({ showWebhookConfigModal: false, webhookConfigId: 0 }) - this.getAllChannelConfigs() - } - - onCloseWebhookModal() { - this.setState({ showWebhookConfigModal: false, webhookConfigId: 0 }) - } - - renderWebhookConfigModal() { - if (this.state.showWebhookConfigModal) { - return ( - - ) - } - } - - deleteConfigPayload(): any { - if (this.state.showDeleteConfigModalType === DeleteComponentsName.SlackConfigurationTab) { - return this.state.slackConfig - } - if (this.state.showDeleteConfigModalType === DeleteComponentsName.SesConfigurationTab) { - return this.state.sesConfig - } - if (this.state.showDeleteConfigModalType === DeleteComponentsName.WebhookConfigurationTab) { - return this.state.webhookConfig - } - return this.state.smtpConfig - } - - deleteConfigComponent(): string { - if (this.state.showDeleteConfigModalType === DeleteComponentsName.SlackConfigurationTab) { - return DeleteComponentsName.SlackConfigurationTab - } - if (this.state.showDeleteConfigModalType === DeleteComponentsName.SesConfigurationTab) { - return DeleteComponentsName.SesConfigurationTab - } - if (this.state.showDeleteConfigModalType === DeleteComponentsName.WebhookConfigurationTab) { - return DeleteComponentsName.WebhookConfigurationTab - } - return DeleteComponentsName.SMTPConfigurationTab - } - - render() { - if (this.state.view === ViewType.LOADING) { - return ( -
- -
- ) - } - if (this.state.view === ViewType.ERROR) { - return ( -
- -
- ) - } - const payload = this.deleteConfigPayload() - return ( - <> -
- {this.renderSESConfigurations()} - {this.renderSMTPConfigurations()} - {this.renderSlackConfigurations()} - {this.renderWebhookConfigurations()} -
- {this.renderSESConfigModal()} - {this.renderSMTPConfigModal()} - {this.renderSlackConfigModal()} - {this.renderWebhookConfigModal()} - {this.state.confirmation && ( - { + try { + await deleteNotification(deletePayload) + // eslint-disable-next-line @typescript-eslint/no-floating-promises + getAllChannelConfigs() + setState({ ...state, confirmation: false }) + } catch (serverError) { + if (serverError instanceof ServerErrors && serverError.code !== 500) { + showError(serverError) + } + setState({ ...state, confirmation: false, showCannotDeleteDialogModal: true }) + } + } + + const handleConfirmation = () => { + setState({ ...state, showCannotDeleteDialogModal: false }) + } + + const renderDeleteModal = () => ( + + ) + + const renderCannotDeleteDialogModal = () => ( + + ) + + const renderTableComponent = () => + + 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 isEmptyView = !state[`${modal.toLowerCase()}ConfigurationList`].length + + return ( +
+ + {isEmptyView ? renderEmptyState() : renderTableComponent()} + {renderModal()} + {renderDeleteModal()} + {renderCannotDeleteDialogModal()} +
+ ) } diff --git a/src/components/notifications/ConfigurationTables.tsx b/src/components/notifications/ConfigurationTables.tsx new file mode 100644 index 0000000000..6bb01d96d5 --- /dev/null +++ b/src/components/notifications/ConfigurationTables.tsx @@ -0,0 +1,93 @@ +import { Switch, Route, useRouteMatch } from 'react-router-dom' +import { DeleteComponentsName } from '@Config/constantMessaging' +import { showError } from '@devtron-labs/devtron-fe-common-lib' +import { ConfigurationsTabTypes } from './constants' +import SESConfigurationTable from './SESConfigurationTable' +import SlackConfigurationTable from './SlackConfigurationTable' +import { SMTPConfigurationTable } from './SMTPConfigurationTable' +import { WebhookConfigurationTable } from './WebhookConfigurationTable' +import { ConfigurationTablesTypes } from './types' +import { + getSlackConfiguration, + getSESConfiguration, + getSMTPConfiguration, + getWebhookConfiguration, +} from './notifications.service' + +export const ConfigurationTables = ({ activeTab, state, setState }: ConfigurationTablesTypes) => { + const { path } = useRouteMatch() + + const deleteClickHandler = (configId, type: ConfigurationsTabTypes) => async () => { + try { + if (type === ConfigurationsTabTypes.SLACK) { + const { result } = await getSlackConfiguration(configId, true) + setState({ + ...state, + slackConfig: { + ...result, + channel: ConfigurationsTabTypes.SLACK, + }, + confirmation: true, + activeTab: ConfigurationsTabTypes.SLACK, + }) + } else if (type === ConfigurationsTabTypes.SES) { + const { result } = await getSESConfiguration(configId) + setState({ + ...state, + sesConfig: { + ...result, + channel: ConfigurationsTabTypes.SES, + }, + confirmation: true, + activeTab: ConfigurationsTabTypes.SES, + }) + } else if (type === ConfigurationsTabTypes.SMTP) { + const { result } = await getSMTPConfiguration(configId) + setState({ + ...state, + smtpConfig: { + ...result, + channel: ConfigurationsTabTypes.SMTP, + }, + confirmation: true, + activeTab: ConfigurationsTabTypes.SMTP, + }) + } else if (type === ConfigurationsTabTypes.WEBHOOK) { + const { result } = await getWebhookConfiguration(configId) + setState({ + ...state, + webhookConfig: { + ...result, + channel: DeleteComponentsName.WebhookConfigurationTab, + }, + confirmation: true, + activeTab: ConfigurationsTabTypes.WEBHOOK, + }) + } + } catch (e) { + showError(e) + } + } + const renderTableComponent = () => { + switch (activeTab) { + case ConfigurationsTabTypes.SES: + return + case ConfigurationsTabTypes.SMTP: + return + case ConfigurationsTabTypes.SLACK: + return + case ConfigurationsTabTypes.WEBHOOK: + return + default: + return null + } + } + + const renderTableRoute = () => ( + + + + ) + + return renderTableRoute() +} diff --git a/src/components/notifications/ConfigurationTabsSwitcher.tsx b/src/components/notifications/ConfigurationTabsSwitcher.tsx new file mode 100644 index 0000000000..6a5c9a9250 --- /dev/null +++ b/src/components/notifications/ConfigurationTabsSwitcher.tsx @@ -0,0 +1,44 @@ +import { useHistory } from 'react-router-dom' +import { useSearchString } from '@devtron-labs/devtron-fe-common-lib' +import { getConfigurationTabTextWithIcon } from './notifications.util' +import { ConfigurationsTabTypes } from './constants' +import { AddConfigurationButton } from './AddConfigurationButton' +import { ConfigurationTabSwitcherType } from './types' + +export const ConfigurationTabSwitcher = ({ isEmptyView }: ConfigurationTabSwitcherType) => { + const history = useHistory() + const { searchParams } = useSearchString() + const queryParams = new URLSearchParams(history.location.search) + const activeTab = queryParams.get('modal') as ConfigurationsTabTypes + + const handleTabClick = (_activeTab: ConfigurationsTabTypes) => () => { + const newParams = { + ...searchParams, + modal: _activeTab, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) + } + + return ( +
+
+ {getConfigurationTabTextWithIcon().map((tab, index) => ( + + ))} +
+ {!isEmptyView && } +
+ ) +} 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..4c702fcdc9 --- /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 = ({ isDefaultDisable, handleCheckbox, isDefault }: DefaultCheckboxProps) => ( + + Set as default configuration to send emails + +) diff --git a/src/components/notifications/EmptyConfigurationView.tsx b/src/components/notifications/EmptyConfigurationView.tsx new file mode 100644 index 0000000000..0b7d2b89ff --- /dev/null +++ b/src/components/notifications/EmptyConfigurationView.tsx @@ -0,0 +1,18 @@ +import { GenericEmptyState } from '@devtron-labs/devtron-fe-common-lib' +import { EmptyConfigurationSubTitle } from './constants' +import { EmptyConfigurationViewProps } from './types' +import { AddConfigurationButton } from './AddConfigurationButton' + +export const EmptyConfigurationView = ({ activeTab, image }: EmptyConfigurationViewProps) => { + const renderButton = () => + return ( + + ) +} diff --git a/src/components/notifications/ModifyRecipientsModal.tsx b/src/components/notifications/ModifyRecipientsModal.tsx index 6c17890ccb..c2151c62cd 100644 --- a/src/components/notifications/ModifyRecipientsModal.tsx +++ b/src/components/notifications/ModifyRecipientsModal.tsx @@ -18,7 +18,7 @@ import { Component } from 'react' import { showError, Progressing, VisibleModal, RadioGroup, RadioGroupItem, ToastVariantType, ToastManager } from '@devtron-labs/devtron-fe-common-lib' import CreatableSelect from 'react-select/creatable' import { ReactComponent as Close } from '../../assets/icons/ic-close.svg' -import { ReactComponent as Slack } from '../../assets/img/slack-logo.svg' +import { ReactComponent as Slack } from '../../assets/icons/slack-logo.svg' import { ReactComponent as Email } from '../../assets/icons/ic-mail.svg' import { ReactComponent as AlertTriangle } from '../../assets/icons/ic-alert-triangle.svg' import { ReactComponent as Webhook } from '../../assets/icons/ic-CIWebhook.svg' diff --git a/src/components/notifications/NotificationTab.tsx b/src/components/notifications/NotificationTab.tsx index 53e09e0d94..43c515356b 100644 --- a/src/components/notifications/NotificationTab.tsx +++ b/src/components/notifications/NotificationTab.tsx @@ -43,7 +43,7 @@ import { ReactComponent as Add } from '../../assets/icons/ic-add.svg' import { ReactComponent as Delete, ReactComponent as Trash } from '../../assets/icons/ic-delete.svg' import { ReactComponent as Bell } from '../../assets/icons/ic-bell.svg' import { ReactComponent as User } from '../../assets/icons/ic-users.svg' -import { ReactComponent as Slack } from '../../assets/img/slack-logo.svg' +import { ReactComponent as Slack } from '../../assets/icons/slack-logo.svg' import { ReactComponent as Email } from '../../assets/icons/ic-mail.svg' import { ReactComponent as Check } from '../../assets/icons/ic-check.svg' import { ReactComponent as Play } from '../../assets/icons/ic-play.svg' diff --git a/src/components/notifications/Notifications.tsx b/src/components/notifications/Notifications.tsx index 017401c0e9..d1aeab4130 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 awsRegionListOption = awsRegionList + .sort((a, b) => stringComparatorBySortOrder(a.name, b.name)) + .map((region) => ({ label: region.name, value: region.value })) -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 SESConfigModal = ({ + sesConfigId, + shouldBeDefault, + selectSESFromChild, + onSaveSuccess, + closeSESConfigModal, +}: SESConfigModalProps) => { + const history = useHistory() + const selectRef = useRef(null) + + const [form, setForm] = useState(getSESDefaultConfiguration(shouldBeDefault)) + const [isFormValid, setFormValid] = useState(DefaultSESValidations) + const [unMaskedSecretKey, setUnMaskedSecretKey] = useState('') -export class SESConfigModal extends Component { - awsRegionListParsed = awsRegionList.map((region) => { - return { label: region.name, value: region.value } - }) + const fetchSESConfiguration = async () => { + setForm((prevForm) => ({ ...prevForm, isLoading: true })) + try { + const response = await getSESConfiguration(sesConfigId) + const { region } = response.result + const awsRegion = awsRegionListOption.find((r) => r.value === region) - constructor(props) { - super(props) - this.state = { - view: ViewType.LOADING, - form: { - configName: '', - accessKey: '', - secretKey: '', - region: { label: '', value: '' }, - fromEmail: '', - default: this.props.shouldBeDefault, + setForm({ + ...response.result, isLoading: false, - isError: true, - }, - isValid: { - configName: true, - accessKey: true, - secretKey: true, - region: true, - fromEmail: true, - }, - secretKey: '', + region: awsRegion, + secretKey: DEFAULT_SECRET_PLACEHOLDER, // Masked secretKey for security + }) + setUnMaskedSecretKey(response.result.secretKey) + setFormValid(DefaultSESValidations) + } catch (error) { + showError(error) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) } - 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) } - 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) + useEffect(() => { + if (sesConfigId) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchSESConfiguration() } - } + }, [sesConfigId]) - 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 + const handleInputChange = (e) => { + const { name, value } = e.target + if (name === ConfigurationFieldKeys.SECRET_KEY) { + setUnMaskedSecretKey(value) } - this.setState({ isValid }) + setForm((prevForm) => ({ + ...prevForm, + [name]: value, + })) + setFormValid((prevValid) => ({ + ...prevValid, + [name]: validateKeyValueConfig(name, value), + })) } - - handleConfigNameChange(event: React.ChangeEvent): void { - const { form } = { ...this.state } - form.configName = event.target.value - this.setState({ form }) + const handleBlur = (event): void => { + const { name, value } = event.target + setFormValid((prevValid) => ({ + ...prevValid, + [name]: validateKeyValueConfig(name as ConfigurationFieldKeys, value), + })) } - handleAccessKeyIDChange(event: React.ChangeEvent): void { - const { form, isValid } = { ...this.state } - form.accessKey = event.target.value - this.setState({ form, isValid }) + const handleAWSRegionChange = (selected: OptionType): void => { + setFormValid((prevValid) => ({ + ...prevValid, + region: validateKeyValueConfig(ConfigurationFieldKeys.REGION, selected.value), + })) + setForm((prevForm) => ({ + ...prevForm, + region: selected, + })) } - 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 handleAWSBlur: SelectPickerProps['onBlur'] = (): void => { + const selectedValue = selectRef.current?.getValue()[0] || {} + setFormValid((prevValid) => ({ + ...prevValid, + region: validateKeyValueConfig(ConfigurationFieldKeys.REGION, selectedValue.value), + })) } - handleAWSRegionChange(event): void { - const { form, isValid } = { ...this.state } - form.region = event - isValid.region = !!event - this.setState({ form, isValid }) + const handleCheckbox = (): void => { + setForm((prevForm) => ({ + ...prevForm, + default: !prevForm.default, + })) } - handleEmailChange(event: React.ChangeEvent): void { - const { form, isValid } = { ...this.state } - form.fromEmail = event.target.value - this.setState({ form, isValid }) + const closeSESConfig = () => { + if (typeof closeSESConfigModal === 'function') { + closeSESConfigModal() + } else { + const newParams = { + modal: ConfigurationsTabTypes.SES, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) + } } - handleCheckbox(event): void { - const { form, isValid } = { ...this.state } - form.default = !form.default - this.setState({ form, isValid }) - } + const validateSave = (): boolean => { + const formConfig = [ + { key: ConfigurationFieldKeys.CONFIG_NAME, value: form.configName }, + { key: ConfigurationFieldKeys.ACCESS_KEY, value: form.accessKey }, + { key: ConfigurationFieldKeys.SECRET_KEY, value: form.secretKey }, + { key: ConfigurationFieldKeys.REGION, value: form.region?.value?.toString() }, + { key: ConfigurationFieldKeys.FROM_EMAIL, value: form.fromEmail }, + ] - getPayload = () => { - return { - ...this.state.form, - region: this.state.form.region.value, - secretKey: this.state.secretKey, - } + const { allValid, formValidations } = getValidationFormConfig(formConfig) + setFormValid((prevValid) => ({ ...prevValid, ...formValidations })) + return allValid } - 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 saveSESConfig = async () => { + if (!validateSave()) { + renderErrorToast() return } - const state = { ...this.state } - state.form.isLoading = true - state.form.isError = false - this.setState(state) + setForm((prevForm) => ({ + ...prevForm, + isLoading: true, + })) - 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) + const payload = { + channel: ConfigurationsTabTypes.SES, + configs: [ + { + configName: form.configName, + accessKey: form.accessKey, + secretKey: unMaskedSecretKey, + region: form.region?.value, + fromEmail: form.fromEmail, + default: form.default, + id: sesConfigId, + }, + ], + } + + try { + const response = await saveEmailConfiguration(payload) + 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 new file mode 100644 index 0000000000..75e613a89f --- /dev/null +++ b/src/components/notifications/SESConfigurationTable.tsx @@ -0,0 +1,62 @@ +import { DeleteComponentsName } from '@Config/constantMessaging' +import { useSearchString } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' +import { ConfigurationTableProps } from './types' +import { ConfigurationsTabTypes } from './constants' +import { getConfigTabIcons, renderDefaultTag, renderText } from './notifications.util' +import './notifications.scss' +import { ConfigTableRowActionButton } from './ConfigTableRowActionButton' + +const SESConfigurationTable = ({ state, deleteClickHandler }: ConfigurationTableProps) => { + const { searchParams } = useSearchString() + const history = useHistory() + + const onClickSESConfigEdit = (id: number) => () => { + const newParams = { + ...searchParams, + configId: id.toString(), + modal: ConfigurationsTabTypes.SES, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) + } + + return ( +
+
+

+

Name

+

Access Key Id

+

Sender's Email

+

+

+
+ {state.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..435aa49aba 100644 --- a/src/components/notifications/SMTPConfigModal.tsx +++ b/src/components/notifications/SMTPConfigModal.tsx @@ -13,309 +13,233 @@ * 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 { + getSMTPDefaultConfiguration, + getValidationFormConfig, + renderErrorToast, + 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]) - handleBlur(event): void { - const { name, value } = event.target - this.setState((prevState) => ({ - ...prevState, - isValid: { ...prevState.isValid, [name]: !!value.length }, - })) + const handleBlur = (e) => { + const { name, value } = e.target + 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 handleInputChange = (e) => { + const { name, value } = e.target + setForm((prevForm) => ({ ...prevForm, [name]: value })) + setFormValid((prevValid) => ({ ...prevValid, [name]: validateKeyValueConfig(name, value) })) } - handleCheckbox(): void { - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, default: !prevState.form.default }, - })) + const handleCheckbox = () => { + setForm((prevForm) => ({ ...prevForm, default: !prevForm.default })) } - 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 }, - })) - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Some required fields are missing or Invalid', + const closeSMTPConfig = () => { + if (typeof closeSMTPConfigModal === 'function') { + closeSMTPConfigModal() + } else { + const newParams = { + modal: ConfigurationsTabTypes.SMTP, + } + history.push({ + search: new URLSearchParams(newParams).toString(), }) - return } - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, isLoading: true, isError: false }, - })) - - saveEmailConfiguration(this.state.form, 'smtp') - .then((response) => { - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, isLoading: false }, - })) - ToastManager.showToast({ - variant: ToastVariantType.success, - description: 'Saved Successfully', - }) - this.props.onSaveSuccess() - if (this.props.selectSMTPFromChild) { - this.props.selectSMTPFromChild(response?.result[0]) - } - }) - .catch((error) => { - showError(error) - this.setState((prevState) => ({ - ...prevState, - form: { ...prevState.form, isLoading: false }, - })) - }) } - renderWithBackdrop(body) { - return ( - -
-
-

Configure SMTP

- -
- {body} -
-
- ) - } + const validateSave = (): boolean => { + const formConfig = [ + { key: ConfigurationFieldKeys.CONFIG_NAME, value: form.configName }, + { key: ConfigurationFieldKeys.HOST, value: form.host }, + { key: ConfigurationFieldKeys.PORT, value: form.port }, + { key: ConfigurationFieldKeys.AUTH_USER, value: form.authUser }, + { key: ConfigurationFieldKeys.AUTH_PASSWORD, value: form.authPassword }, + { key: ConfigurationFieldKeys.FROM_EMAIL, value: form.fromEmail }, + ] - onSaveClickHandler(event) { - event.preventDefault() - this.saveSMTPConfig() + const { allValid, formValidations } = getValidationFormConfig(formConfig) + setFormValid((prevValid) => ({ ...prevValid, ...formValidations })) + return allValid } - render() { - let body - if (this.state.view === ViewType.LOADING) { - body = ( -
- -
- ) - } else { - body = ( - <> -
- - - -
- -
-
- -
- -
-
- - Set as default configuration to send emails - -
- - -
-
- - ) + const saveSMTPConfig = async () => { + if (!validateSave()) { + renderErrorToast() + return + } + setForm((prevForm) => ({ ...prevForm, isLoading: true })) + + const payload = { + channel: ConfigurationsTabTypes.SES, + configs: [ + { + configName: form.configName, + host: form.host, + port: form.port, + authUser: form.authUser, + authPassword: form.authPassword, + fromEmail: form.fromEmail, + default: form.default, + id: smtpConfigId, + }, + ], + } + + try { + const response = await saveEmailConfiguration(payload) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) + ToastManager.showToast({ + variant: ToastVariantType.success, + description: 'Saved Successfully', + }) + onSaveSuccess() + closeSMTPConfig() + if (selectSMTPFromChild) { + selectSMTPFromChild(response?.result[0]) + } + } catch (error) { + showError(error) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) } - return this.renderWithBackdrop(body) } + + const renderForm = () => ( +
+ + + + + +
+ +
+ + +
+ ) + + return ( + + ) } diff --git a/src/components/notifications/SMTPConfigurationTable.tsx b/src/components/notifications/SMTPConfigurationTable.tsx new file mode 100644 index 0000000000..e6115f1ab1 --- /dev/null +++ b/src/components/notifications/SMTPConfigurationTable.tsx @@ -0,0 +1,66 @@ +import { DeleteComponentsName } from '@Config/constantMessaging' +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 { getConfigTabIcons, renderDefaultTag, renderText } from './notifications.util' +import { ConfigTableRowActionButton } from './ConfigTableRowActionButton' + +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(), + }) + } + + return ( +
+
+

+

Name

+

Host

+

Port

+

Sender' Email

+

+

+
+ {smtpConfigurationList.map((smtpConfig) => ( +
+ {getConfigTabIcons(ConfigurationsTabTypes.SMTP)} +
+ {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..23fdd617af 100644 --- a/src/components/notifications/SlackConfigModal.tsx +++ b/src/components/notifications/SlackConfigModal.tsx @@ -1,353 +1,251 @@ -/* - * 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 { saveSlackConfiguration, updateSlackConfiguration, getSlackConfiguration } from './notifications.service' +import { useHistory } from 'react-router-dom' +import { saveSlackConfiguration, 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, + SlackIncomingWebhookUrl, +} from './constants' +import { ConfigurationTabDrawerModal } from './ConfigurationDrawerModal' +import { getValidationFormConfig, 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 })) }) } - } + }, [slackConfigId]) - handleSlackChannelChange(event: React.ChangeEvent): void { - const { form } = { ...this.state } - form.configName = event.target.value - this.setState({ form }) - } - - 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 }) + const validateSave = (): boolean => { + const formConfig = [ + { key: ConfigurationFieldKeys.CONFIG_NAME, value: form.configName }, + { key: ConfigurationFieldKeys.WEBHOOK_URL, value: form.webhookUrl }, + { key: ConfigurationFieldKeys.PROJECT_ID, value: selectedProject?.value ?? '' }, + ] + const { allValid, formValidations } = getValidationFormConfig(formConfig) + setFormValid((prevValid) => ({ ...prevValid, ...formValidations })) + return allValid } - 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 }) - } - - 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 = async () => { + if (!validateSave()) { + renderErrorToast() return } - const requestBody = this.state.form - if (this.props.slackConfigId) { - requestBody['id'] = this.props.slackConfigId + + const requestBody = { + channel: ConfigurationsTabTypes.SLACK, + configs: [ + { + id: slackConfigId, + configName: form.configName, + webhookUrl: form.webhookUrl, + teamId: form.projectId, + }, + ], } - const promise = this.props.slackConfigId - ? updateSlackConfiguration(requestBody) - : saveSlackConfiguration(requestBody) - promise - .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) + setForm((prevForm) => ({ ...prevForm, isLoading: true })) + + try { + await saveSlackConfiguration(requestBody) + setForm((prevForm) => ({ ...prevForm, isLoading: false })) + ToastManager.showToast({ + variant: ToastVariantType.success, + description: 'Saved Successfully', }) + 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 new file mode 100644 index 0000000000..3752cb352f --- /dev/null +++ b/src/components/notifications/SlackConfigurationTable.tsx @@ -0,0 +1,60 @@ +import { DeleteComponentsName } from '@Config/constantMessaging' +import { useSearchString } from '@devtron-labs/devtron-fe-common-lib' +import { useHistory } from 'react-router-dom' +import { ConfigurationTableProps } from './types' +import { ConfigurationsTabTypes } from './constants' +import { getConfigTabIcons, renderText } from './notifications.util' +import './notifications.scss' +import { ConfigTableRowActionButton } from './ConfigTableRowActionButton' + +const SlackConfigurationTable = ({ state, deleteClickHandler }: ConfigurationTableProps) => { + const { searchParams } = useSearchString() + const history = useHistory() + const { slackConfigurationList } = state + + const onClickSlackConfigEdit = (id: number) => () => { + const newParams = { + ...searchParams, + configId: id.toString(), + modal: ConfigurationsTabTypes.SLACK, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) + } + + return ( +
+
+
+

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..b6793e26e7 --- /dev/null +++ b/src/components/notifications/WebhookConfigDynamicDataTable.tsx @@ -0,0 +1,72 @@ +import { DynamicDataTable } from '@devtron-labs/devtron-fe-common-lib' +import { VariableDataTableActionType } from '@Components/CIPipelineN/VariableDataTable/types' +import { getEmptyVariableDataRow, getTableHeaders } from './notifications.util' +import { + HandleRowUpdateActionProps, + WebhookConfigDynamicDataTableProps, + WebhookDataRowType, + WebhookHeaderKeyType, +} from './types' + +export const WebhookConfigDynamicDataTable = ({ rows, setRows }: WebhookConfigDynamicDataTableProps) => { + 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) => { + 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..71b514d828 100644 --- a/src/components/notifications/WebhookConfigModal.tsx +++ b/src/components/notifications/WebhookConfigModal.tsx @@ -13,419 +13,265 @@ * 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 { + getEmptyVariableDataRow, + getInitialWebhookKeyRow, + getValidationFormConfig, + renderErrorToast, + validateKeyValueConfig, + validatePayloadField, +} from './notifications.util' + +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({}) -export class WebhookConfigModal extends Component { - constructor(props) { - super(props) - this.state = { - view: ViewType.LOADING, - form: { - configName: '', - webhookUrl: '', + const fetchWebhookData = async () => { + setForm((prev) => ({ ...prev, isLoading: true })) + try { + // Fetch webhook configuration + const response = await getWebhookConfiguration(webhookConfigId) + const { header = {}, payload = '' } = response?.result || {} + // Update form state with response data + setForm((prev) => ({ + ...prev, + ...response?.result, + header, + payload, isLoading: false, - isError: false, - payload: '', - header: [{ key: '', value: '' }], - }, - isValid: { - configName: true, - webhookUrl: true, - payload: true, - }, - webhookAttribute: {}, - copyAttribute: false, + })) + setRows(getInitialWebhookKeyRow(header)) + } 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() + } else { + setRows([getEmptyVariableDataRow()]) } - this.setState({ form, isValid }) - } - - handleWebhookUrlChange(event: React.ChangeEvent): void { - const { form } = { ...this.state } - form.webhookUrl = event.target.value - this.setState({ form }) - } + }, [webhookConfigId]) - handleWebhookPaylodChange(value): void { - const { form } = { ...this.state } - form.payload = value - this.setState({ form }) - } - - 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 handleIncomingPayloadChange = (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 validateSave = (): boolean => { + const formConfig = [ + { key: ConfigurationFieldKeys.CONFIG_NAME, value: form.configName }, + { key: ConfigurationFieldKeys.WEBHOOK_URL, value: form.webhookUrl }, + { key: ConfigurationFieldKeys.PAYLOAD, value: form.payload }, + ] + const { allValid, formValidations } = getValidationFormConfig(formConfig) + setFormValid((prevValid) => ({ ...prevValid, ...formValidations })) + return allValid } - renderWithBackdrop(body) { - return ( - -
-
-

Configure Webhook

- -
- {body} -
-
- ) - } + const saveWebhookConfig = async () => { + if (!validateSave()) { + renderErrorToast() + return + } - onSaveClickHandler(event) { - event.preventDefault() - this.saveWebhookConfig() - } + const headers = rows?.reduce((acc, row) => { + acc[row.data.key.value] = row.data.value.value + return acc + }, {}) + setForm((prev) => ({ ...prev, isLoading: true })) - onClickSave(event) { - event.preventDefault() - this.saveWebhookConfig() + try { + const requestBody = { + channel: ConfigurationsTabTypes.WEBHOOK, + configs: [ + { + configName: form.configName, + webhookUrl: form.webhookUrl, + payload: form.payload, + id: webhookConfigId, + header: headers, + }, + ], + } + 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 new file mode 100644 index 0000000000..debeb80c17 --- /dev/null +++ b/src/components/notifications/WebhookConfigurationTable.tsx @@ -0,0 +1,61 @@ +import { DeleteComponentsName } from '@Config/constantMessaging' +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 { ConfigTableRowActionButton } from './ConfigTableRowActionButton' +import { getConfigTabIcons, renderText } from './notifications.util' + +export const WebhookConfigurationTable = ({ state, deleteClickHandler }: ConfigurationTableProps) => { + const { webhookConfigurationList } = state + const { searchParams } = useSearchString() + const history = useHistory() + + const onClickWebhookConfigEdit = (id: number) => () => { + const newParams = { + ...searchParams, + configId: id.toString(), + modal: ConfigurationsTabTypes.WEBHOOK, + } + history.push({ + search: new URLSearchParams(newParams).toString(), + }) + } + + return ( +
+
+

+

Name

+

Webhook URL

+

+

+
+ {webhookConfigurationList.map((webhookConfig) => ( +
+ {getConfigTabIcons(ConfigurationsTabTypes.WEBHOOK)} + {renderText( + webhookConfig.name, + true, + onClickWebhookConfigEdit(webhookConfig.id), + `webhook-config-name-${webhookConfig.name}`, + )} + {renderText(webhookConfig.webhookUrl, false, noop, `webhook-url-${webhookConfig.webhookUrl}`)} + +
+ ))} +
+
+ ) +} diff --git a/src/components/notifications/constants.ts b/src/components/notifications/constants.ts new file mode 100644 index 0000000000..79bd225b4e --- /dev/null +++ b/src/components/notifications/constants.ts @@ -0,0 +1,106 @@ +// ------------ Configuration Constants ------------ + +import { SlackFormType } from './types' + +export enum ConfigurationsTabTypes { + SES = 'ses', + SMTP = 'smtp', + SLACK = 'slack', + 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', + [ConfigurationsTabTypes.SMTP]: + 'SMTP configuration will be used to send email notifications to the desired email address', + [ConfigurationsTabTypes.SLACK]: 'Configure slack webhook to send notifications to a slack channel', + [ConfigurationsTabTypes.WEBHOOK]: 'Configure webhook to send event data to external tools', +} + +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 DefaultWebhookConfig = { + [ConfigurationFieldKeys.CONFIG_NAME]: '', + [ConfigurationFieldKeys.WEBHOOK_URL]: '', + [ConfigurationFieldKeys.PAYLOAD]: '', + isLoading: false, + header: {}, +} + +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..3c4aef7aa6 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,56 @@ 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; -} - -.smtp-config-table__host { - flex-basis: 33%; - font-weight: inherit; -} + .ses-config-container { + .ses-config-grid { + display: grid; + grid-template-columns: 24px 250px 1fr 1fr 52px; + } + } -.smtp-config-table__port { - flex-basis: 5%; - font-weight: inherit; -} + .slack-config-container { + .slack-config-grid { + display: grid; + grid-template-columns: 24px 250px 1fr 52px; + } + } -.smtp-config-table__email { - flex-basis: 30%; - font-weight: inherit; -} + .smtp-config-container { + .smtp-config-grid { + display: grid; + grid-template-columns: 24px 250px 1fr 60px 1fr 52px; + } + } -.slack-config-table__name { - flex-basis: 25%; - color: var(--N900); - font-weight: inherit; -} + .webhook-config-container { + .webhook-config-grid { + display: grid; + grid-template-columns: 24px 250px 1fr 52px; + } + } -.slack-config-table__webhook { - flex-basis: 60%; -} -.slack-config-table__action, -.ses-config-table__action { - display: none; - margin: auto; - margin-right: 0; } .form__row--ses-account { @@ -234,12 +187,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 +331,7 @@ } } -.data-conatiner { +.data-container { .hover-only { display: none; } @@ -404,12 +351,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.service.ts b/src/components/notifications/notifications.service.ts index 4a519430c8..29b09dbca0 100644 --- a/src/components/notifications/notifications.service.ts +++ b/src/components/notifications/notifications.service.ts @@ -342,12 +342,8 @@ export function deleteNotifications(requestBody, singleDeletedId): Promise { +export function saveEmailConfiguration(payload): Promise { const URL = `${Routes.NOTIFIER}/channel` - const payload = { - channel, - configs: [data], - } return post(URL, payload) } @@ -386,58 +382,12 @@ export function getWebhookConfiguration(webhookConfigId: number): Promise { - const headerObj = {} - const headerPayload = data.payload !== '' ? data.payload : '' - data.header.forEach((element) => { - if (element.key != '') { - headerObj[element.key] = element.value - } - }) - - const payload = { - channel: 'webhook', - configs: [ - { - id: Number(data.id), - configName: data.configName, - webhookUrl: data.webhookUrl, - header: headerObj, - payload: headerPayload, - }, - ], - } +export function saveUpdateWebhookConfiguration(payload): Promise { return post(`${Routes.NOTIFIER}/channel`, payload) } -export function saveSlackConfiguration(data): Promise { - const URL = `${Routes.NOTIFIER}/channel` - const payload = { - channel: 'slack', - configs: [ - { - configName: data.configName, - webhookUrl: data.webhookUrl, - teamId: data.projectId, - }, - ], - } - return post(URL, payload) -} - -export function updateSlackConfiguration(data): Promise { +export function saveSlackConfiguration(payload): Promise { const URL = `${Routes.NOTIFIER}/channel` - const payload = { - channel: 'slack', - configs: [ - { - id: data.id, - configName: data.configName, - webhookUrl: data.webhookUrl, - teamId: data.projectId, - }, - ], - } return post(URL, payload) } diff --git a/src/components/notifications/notifications.util.tsx b/src/components/notifications/notifications.util.tsx index 97f24d64eb..ece18b575f 100644 --- a/src/components/notifications/notifications.util.tsx +++ b/src/components/notifications/notifications.util.tsx @@ -15,15 +15,28 @@ */ import { components } from 'react-select' +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' +import { ReactComponent as CI } from '@Icons/ic-CI.svg' +import { ReactComponent as CD } from '@Icons/ic-CD.svg' +import { ReactComponent as Rocket } from '@Icons/ic-paper-rocket.svg' +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 { + DynamicDataTableHeaderType, + DynamicDataTableRowDataType, + getUniqueId, + ToastManager, + ToastVariantType, + Tooltip, +} from '@devtron-labs/devtron-fe-common-lib' +import { ConfigurationFieldKeys, ConfigurationsTabTypes, ConfigurationTabText } from './constants' import { validateEmail } from '../common' -import { ReactComponent as ArrowDown } from '../../assets/icons/ic-chevron-down.svg' -import { ReactComponent as Slack } from '../../assets/img/slack-logo.svg' -import { ReactComponent as Webhook } from '../../assets/icons/ic-CIWebhook.svg' -import { ReactComponent as Email } from '../../assets/icons/ic-mail.svg' -import { ReactComponent as RedWarning } from '../../assets/icons/ic-error-medium.svg' -import { ReactComponent as CI } from '../../assets/icons/ic-CI.svg' -import { ReactComponent as CD } from '../../assets/icons/ic-CD.svg' -import { ReactComponent as Rocket } from '../../assets/icons/ic-paper-rocket.svg' +import { FormError, SESFormType, SMTPFormType, WebhookDataRowType, WebhookHeaderKeyType } from './types' +import { REQUIRED_FIELD_MSG } from '@Config/constantMessaging' export const multiSelectStyles = { control: (base, state) => ({ @@ -36,60 +49,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 @@ -160,3 +161,208 @@ export const renderPipelineTypeIcon = (row) => { } return } + +export const getConfigTabIcons = (tab: ConfigurationsTabTypes, size: number = 24) => { + switch (tab) { + case ConfigurationsTabTypes.SMTP: + return + case ConfigurationsTabTypes.SLACK: + return + case ConfigurationsTabTypes.WEBHOOK: + return + case ConfigurationsTabTypes.SES: + default: + return + } +} + +export const getConfigurationTabTextWithIcon = () => [ + { + label: ConfigurationTabText.SES, + icon: getConfigTabIcons(ConfigurationsTabTypes.SES, 20), + link: ConfigurationsTabTypes.SES, + }, + { + label: ConfigurationTabText.SMTP, + icon: getConfigTabIcons(ConfigurationsTabTypes.SMTP, 20), + link: ConfigurationsTabTypes.SMTP, + }, + { + label: ConfigurationTabText.SLACK, + icon: getConfigTabIcons(ConfigurationsTabTypes.SLACK, 20), + link: ConfigurationsTabTypes.SLACK, + }, + { + label: ConfigurationTabText.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 = (headers: { key: string; value: string }): WebhookDataRowType[] => { + return Object.entries(headers).map(([key, value]) => { + return { + data: { + key: { + value: key || null, + type: DynamicDataTableRowDataType.TEXT, + props: { + placeholder: 'Eg. owner-name', + }, + }, + value: { + value: value || '', + type: DynamicDataTableRowDataType.TEXT, + props: { + placeholder: 'Enter value', + }, + }, + }, + + id: getUniqueId(), + } + }) +} + +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 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', + }) + +export const getValidationFormConfig = (formConfig) => { + const { allValid, formValidations } = formConfig.reduce( + (acc, { key, value }) => { + const validation = validateKeyValueConfig(key, value) + acc.formValidations[key] = validation + if (!validation.isValid) { + acc.allValid = false + } + return acc + }, + { allValid: true, formValidations: {} }, + ) + return { allValid, formValidations } +} \ No newline at end of file diff --git a/src/components/notifications/types.tsx b/src/components/notifications/types.tsx index 8cb08e1efe..3be8e9ad9a 100644 --- a/src/components/notifications/types.tsx +++ b/src/components/notifications/types.tsx @@ -15,7 +15,14 @@ */ import { RouteComponentProps } from 'react-router-dom' -import { ServerError, ResponseType } from '@devtron-labs/devtron-fe-common-lib' +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 }> {} @@ -25,6 +32,7 @@ export interface NotifierState { successMessage: string | null channel: string } + export interface SMTPConfigResponseType extends ResponseType { result?: { configName: string @@ -47,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 @@ -94,53 +74,176 @@ 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 { + state: ConfigurationTabState + deleteClickHandler: (id: number, name: string) => void +} + +export interface ConfigurationTablesTypes { + activeTab: ConfigurationsTabTypes + state: ConfigurationTabState + setState: React.Dispatch> +} + +export interface FormError { + isValid: boolean + message: string +} + +export interface ConfigurationTabDrawerModalProps { + renderContent: () => JSX.Element + closeModal: () => void + modal: ConfigurationsTabTypes + isLoading: boolean + saveConfigModal: () => void + disableSave?: boolean +} + +export interface DefaultCheckboxProps { + isDefaultDisable: boolean + handleCheckbox: () => void + isDefault: boolean +} + +// ----------------------------Configuration Tab---------------------------- + +export interface EmptyConfigurationViewProps { + activeTab: ConfigurationsTabTypes + image?: any +} + +export interface ConfigurationTabSwitcherProps { + activeTab: ConfigurationsTabTypes +} + +export interface ConfigTableRowActionButtonProps { + onClickEditRow: () => void + onClickDeleteRow: any + 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 type WebhookDataRowType = DynamicDataTableRowType -export interface HeaderType { +export interface WebhookHeadersType { key: string value: string } -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> } -export interface WebhookAttributesResponseType extends ResponseType { - result?: Record +type VariableDataTableActionPropsMap = { + [VariableDataTableActionType.UPDATE_ROW]: string +} + +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 type WebhookValidations = { + [ConfigurationFieldKeys.CONFIG_NAME]: FormError + [ConfigurationFieldKeys.WEBHOOK_URL]: FormError + [ConfigurationFieldKeys.PAYLOAD]: FormError +} + +export interface WebhookFormTypes { + configName: string + webhookUrl: string + isLoading: boolean + payload: string + header: Object +} + +export interface AddConfigurationButtonProps { + activeTab: ConfigurationsTabTypes +} + +export interface ConfigurationTabSwitcherType { + isEmptyView: boolean } diff --git a/src/css/base.scss b/src/css/base.scss index a3799ba8d4..deed5922db 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; } @@ -4387,6 +4389,10 @@ textarea::placeholder { background-color: var(--N50); } +.dc__hover-text-n90:hover { + color: var(--N900) !important; +} + .dc__bg-g5 { background-color: var(--G500) !important; }