${tw`sw-py-2 sw-px-4`}
${tw`sw-box-border`}
${tw`sw-rounded-b-2`}
+ ${tw`sw-w-full`}
+ ${tw`sw-text-left`}
color: ${themeContrast('infoBackground')};
background-color: ${themeColor('infoBackground')};
"@react-spring/web": "9.7.3",
"@sonarsource/echoes-react": "0.6.0",
"@tanstack/react-query": "5.18.1",
+ "@types/validator": "13.12.0",
"axios": "1.7.2",
"classnames": "2.5.1",
"clipboard": "2.0.11",
"react-virtualized": "9.22.5",
"regenerator-runtime": "0.14.1",
"shared-store-hook": "0.0.4",
- "valid-url": "1.0.9"
+ "valid-url": "1.0.9",
+ "validator": "13.12.0"
},
"devDependencies": {
"@emotion/jest": "11.11.0",
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { cloneDeep } from 'lodash';
+import { cloneDeep, uniqueId } from 'lodash';
import { Provider, SysInfoCluster, SysInfoLogging, SysInfoStandalone } from '../../types/types';
import { LogsLevels } from '../../apps/system/utils';
-import { mockClusterSysInfo, mockLogs, mockStandaloneSysInfo } from '../../helpers/testMocks';
-import { getSystemInfo, getSystemUpgrades, setLogLevel } from '../system';
+import { mockEmailConfiguration } from '../../helpers/mocks/system';
+import {
+ mockClusterSysInfo,
+ mockLogs,
+ mockPaging,
+ mockStandaloneSysInfo,
+} from '../../helpers/testMocks';
+import { EmailConfiguration } from '../../types/system';
+import {
+ getEmailConfigurations,
+ getSystemInfo,
+ getSystemUpgrades,
+ patchEmailConfiguration,
+ postEmailConfiguration,
+ setLogLevel,
+} from '../system';
jest.mock('../system');
installedVersionActive: true,
};
+ emailConfigurations: EmailConfiguration[] = [];
+
constructor() {
this.updateSystemInfo();
jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
jest.mocked(setLogLevel).mockImplementation(this.handleSetLogLevel);
jest.mocked(getSystemUpgrades).mockImplementation(this.handleGetSystemUpgrades);
+ jest.mocked(getEmailConfigurations).mockImplementation(this.handleGetEmailConfigurations);
+ jest.mocked(postEmailConfiguration).mockImplementation(this.handlePostEmailConfiguration);
}
handleGetSystemInfo = () => {
this.updateSystemInfo();
};
+ handleGetEmailConfigurations: typeof getEmailConfigurations = () => {
+ return this.reply({
+ emailConfigurations: this.emailConfigurations,
+ page: mockPaging({ total: this.emailConfigurations.length }),
+ });
+ };
+
+ handlePostEmailConfiguration: typeof postEmailConfiguration = (configuration) => {
+ const returnVal = mockEmailConfiguration(configuration.authMethod, {
+ ...configuration,
+ id: uniqueId('email-configuration-'),
+ });
+
+ this.emailConfigurations.push(returnVal);
+ return this.reply(returnVal);
+ };
+
+ handlePatchEmailConfiguration: typeof patchEmailConfiguration = (id, configuration) => {
+ const index = this.emailConfigurations.findIndex((c) => c.id === id);
+ this.emailConfigurations[index] = mockEmailConfiguration(configuration.authMethod, {
+ ...this.emailConfigurations[index],
+ ...configuration,
+ });
+ return this.reply(this.emailConfigurations[index]);
+ };
+
+ addEmailConfiguration = (configuration: EmailConfiguration) => {
+ this.emailConfigurations.push(configuration);
+ };
+
reset = () => {
this.logging = mockLogs();
this.setIsCluster(false);
this.updateSystemInfo();
+ this.emailConfigurations = [];
};
reply<T>(response: T): Promise<T> {
import { throwGlobalError } from '~sonar-aligned/helpers/error';
import { getJSON } from '~sonar-aligned/helpers/request';
import { post, postJSON, requestTryAndRepeatUntil } from '../helpers/request';
-import { MigrationStatus, MigrationsStatusResponse, SystemUpgrade } from '../types/system';
-import { SysInfoCluster, SysInfoStandalone, SysStatus } from '../types/types';
+import {
+ EmailConfiguration,
+ MigrationStatus,
+ MigrationsStatusResponse,
+ SystemUpgrade,
+} from '../types/system';
+import { Paging, SysInfoCluster, SysInfoStandalone, SysStatus } from '../types/types';
const MIGRATIONS_STATUS_ENDPOINT = '/api/v2/system/migrations-status';
+const EMAIL_NOTIFICATION_PATH = '/api/v2/system/email-configurations';
export function setLogLevel(level: string): Promise<void | Response> {
return post('/api/system/change_log_level', { level }).catch(throwGlobalError);
({ status }) => status === 'UP',
);
}
+
+export function getEmailConfigurations(): Promise<{
+ emailConfigurations: EmailConfiguration[];
+ page: Paging;
+}> {
+ return axios.get(EMAIL_NOTIFICATION_PATH);
+}
+
+export function postEmailConfiguration(data: EmailConfiguration): Promise<EmailConfiguration> {
+ return axios.post<EmailConfiguration, EmailConfiguration>(EMAIL_NOTIFICATION_PATH, data);
+}
+
+export function patchEmailConfiguration(
+ id: string,
+ emailConfiguration: EmailConfiguration,
+): Promise<EmailConfiguration> {
+ return axios.patch<EmailConfiguration, EmailConfiguration>(
+ `${EMAIL_NOTIFICATION_PATH}/${id}`,
+ emailConfiguration,
+ );
+}
},
{
key: EMAIL_NOTIFICATION_CATEGORY,
- name: translate('settings.email_notification.category'),
+ name: translate('email_notification.category'),
renderComponent: getEmailNotificationComponent,
availableGlobally: true,
availableForProject: false,
return props.component && <PullRequestDecorationBinding component={props.component} />;
}
-function getEmailNotificationComponent(props: AdditionalCategoryComponentProps) {
- return <EmailNotification {...props} />;
+function getEmailNotificationComponent() {
+ return <EmailNotification />;
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { BasicSeparator, Note, SelectionCard } from 'design-system/lib';
+import React from 'react';
+import { translate } from '../../../../helpers/l10n';
+import { AuthMethod } from '../../../../types/system';
+import { EmailNotificationFormField } from './EmailNotificationFormField';
+import {
+ BASIC_PASSWORD,
+ EmailNotificationGroupProps,
+ OAUTH_AUTHENTICATION_HOST,
+ OAUTH_CLIENT_ID,
+ OAUTH_CLIENT_SECRET,
+ OAUTH_TENANT,
+ USERNAME,
+} from './utils';
+
+export function AuthenticationSelector(props: Readonly<EmailNotificationGroupProps>) {
+ const { configuration, onChange } = props;
+
+ const isOAuth = configuration?.authMethod === AuthMethod.OAuth;
+
+ return (
+ <div className="sw-pt-6">
+ <div className="sw-pb-6 sw-flex sw-gap-4 sw-space-between">
+ <SelectionCard
+ className="sw-w-full"
+ selected={!isOAuth}
+ onClick={() => onChange({ authMethod: AuthMethod.Basic })}
+ title={translate('email_notification.form.basic_auth.title')}
+ >
+ <Note>{translate('email_notification.form.basic_auth.description')}</Note>
+ </SelectionCard>
+ <SelectionCard
+ className="sw-w-full"
+ selected={isOAuth}
+ onClick={() => onChange({ authMethod: AuthMethod.OAuth })}
+ recommended
+ recommendedReason={translate('email_notification.form.oauth_auth.recommended_reason')}
+ title={translate('email_notification.form.oauth_auth.title')}
+ >
+ <Note>{translate('email_notification.form.oauth_auth.description')}</Note>
+ </SelectionCard>
+ </div>
+ <BasicSeparator />
+ <EmailNotificationFormField
+ description={translate('email_notification.form.username.description')}
+ id={USERNAME}
+ onChange={(value) => onChange({ username: value })}
+ name={translate('email_notification.form.username')}
+ required
+ value={configuration.username}
+ />
+ <BasicSeparator />
+ {isOAuth ? (
+ <>
+ <EmailNotificationFormField
+ description={translate('email_notification.form.oauth_authentication_host.description')}
+ id={OAUTH_AUTHENTICATION_HOST}
+ onChange={(value) => onChange({ oauthAuthenticationHost: value })}
+ name={translate('email_notification.form.oauth_authentication_host')}
+ required
+ value={
+ configuration.authMethod === AuthMethod.OAuth
+ ? configuration.oauthAuthenticationHost
+ : ''
+ }
+ />
+ <BasicSeparator />
+ <EmailNotificationFormField
+ description={translate('email_notification.form.oauth_client_id.description')}
+ id={OAUTH_CLIENT_ID}
+ onChange={(value) => onChange({ oauthClientId: value })}
+ name={translate('email_notification.form.oauth_client_id')}
+ required
+ type="password"
+ value={configuration.authMethod === AuthMethod.OAuth ? configuration.oauthClientId : ''}
+ />
+ <BasicSeparator />
+ <EmailNotificationFormField
+ description={translate('email_notification.form.oauth_client_secret.description')}
+ id={OAUTH_CLIENT_SECRET}
+ onChange={(value) => onChange({ oauthClientSecret: value })}
+ name={translate('email_notification.form.oauth_client_secret')}
+ required
+ type="password"
+ value={
+ configuration.authMethod === AuthMethod.OAuth ? configuration.oauthClientSecret : ''
+ }
+ />
+ <BasicSeparator />
+ <EmailNotificationFormField
+ description={translate('email_notification.form.oauth_tenant.description')}
+ id={OAUTH_TENANT}
+ onChange={(value) => onChange({ oauthTenant: value })}
+ name={translate('email_notification.form.oauth_tenant')}
+ required
+ value={configuration.authMethod === AuthMethod.OAuth ? configuration.oauthTenant : ''}
+ />
+ </>
+ ) : (
+ <EmailNotificationFormField
+ description={translate('email_notification.form.basic_password.description')}
+ id={BASIC_PASSWORD}
+ onChange={(value) => onChange({ basicPassword: value })}
+ name={translate('email_notification.form.basic_password')}
+ required
+ type="password"
+ value={configuration.authMethod === AuthMethod.Basic ? configuration.basicPassword : ''}
+ />
+ )}
+ <BasicSeparator />
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { BasicSeparator } from 'design-system/lib';
+import React from 'react';
+import { translate } from '../../../../helpers/l10n';
+import { EmailNotificationFormField } from './EmailNotificationFormField';
+import { EmailNotificationGroupProps, HOST, PORT, SECURITY_PROTOCOL } from './utils';
+
+export function CommonSMTP(props: Readonly<EmailNotificationGroupProps>) {
+ const { configuration, onChange } = props;
+
+ return (
+ <div className="sw-pt-6">
+ <EmailNotificationFormField
+ description={translate('email_notification.form.host.description')}
+ id={HOST}
+ onChange={(value) => onChange({ host: value })}
+ name={translate('email_notification.form.host')}
+ required
+ value={configuration.host}
+ />
+ <BasicSeparator />
+ <EmailNotificationFormField
+ description={translate('email_notification.form.port.description')}
+ id={PORT}
+ onChange={(value) => onChange({ port: value })}
+ name={translate('email_notification.form.port')}
+ required
+ type="number"
+ value={configuration.port}
+ />
+ <BasicSeparator />
+ <EmailNotificationFormField
+ description={translate('email_notification.form.security_protocol.description')}
+ id={SECURITY_PROTOCOL}
+ onChange={(value) => onChange({ securityProtocol: value })}
+ name={translate('email_notification.form.security_protocol')}
+ options={['NONE', 'STARTTLS', 'SSLTLS']}
+ required
+ type="select"
+ value={configuration.securityProtocol}
+ />
+ <BasicSeparator />
+ </div>
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { SubTitle } from 'design-system';
+import { Spinner } from '@sonarsource/echoes-react';
+import { SubTitle } from 'design-system/lib';
import React from 'react';
import { FormattedMessage } from 'react-intl';
-import { AdditionalCategoryComponentProps } from '../AdditionalCategories';
-import CategoryDefinitionsList from '../CategoryDefinitionsList';
-import EmailForm from './EmailForm';
+import { useGetEmailConfiguration } from '../../../../queries/system';
+import EmailNotificationConfiguration from './EmailNotificationConfiguration';
-export default function EmailNotification(props: Readonly<AdditionalCategoryComponentProps>) {
- const { component, definitions } = props;
+export default function EmailNotification() {
+ const { data: configuration, isLoading } = useGetEmailConfiguration();
return (
- <div>
+ <div className="sw-p-6">
<SubTitle as="h3">
- <FormattedMessage id="settings.email_notification.header" />
+ <FormattedMessage id="email_notification.header" />
</SubTitle>
- <CategoryDefinitionsList
- category="general"
- component={component}
- definitions={definitions}
- displaySubCategoryTitle={false}
- noPadding
- subCategory="email"
- />
- <EmailForm />
+ <FormattedMessage id="email_notification.description" />
+ <Spinner isLoading={isLoading}>
+ <EmailNotificationConfiguration emailConfiguration={configuration ?? null} />
+ </Spinner>
</div>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { Button, ButtonVariety } from '@sonarsource/echoes-react';
+import { NumberedList, NumberedListItem } from 'design-system/lib';
+import React, { useCallback, useEffect } from 'react';
+import { translate } from '../../../../helpers/l10n';
+import {
+ useSaveEmailConfigurationMutation,
+ useUpdateEmailConfigurationMutation,
+} from '../../../../queries/system';
+import { AuthMethod, EmailConfiguration } from '../../../../types/system';
+import { AuthenticationSelector } from './AuthenticationSelector';
+import { CommonSMTP } from './CommonSMTP';
+import { SenderInformation } from './SenderInformation';
+import { checkEmailConfigurationValidity } from './utils';
+
+interface Props {
+ emailConfiguration: EmailConfiguration | null;
+}
+
+const FORM_ID = 'email-notifications';
+const EMAIL_CONFIGURATION_DEFAULT: EmailConfiguration = {
+ authMethod: AuthMethod.Basic,
+ basicPassword: '',
+ fromAddress: '',
+ fromName: 'SonarQube',
+ host: '',
+ port: '587',
+ securityProtocol: '',
+ subjectPrefix: '[SonarQube]',
+ username: '',
+};
+
+export default function EmailNotificationConfiguration(props: Readonly<Props>) {
+ const { emailConfiguration } = props;
+ const [canSave, setCanSave] = React.useState(false);
+
+ const [newConfiguration, setNewConfiguration] = React.useState<EmailConfiguration>(
+ EMAIL_CONFIGURATION_DEFAULT,
+ );
+
+ const { mutateAsync: saveEmailConfiguration } = useSaveEmailConfigurationMutation();
+ const { mutateAsync: updateEmailConfiguration } = useUpdateEmailConfigurationMutation();
+
+ const onChange = useCallback(
+ (newValue: Partial<EmailConfiguration>) => {
+ const newConfig = {
+ ...newConfiguration,
+ ...newValue,
+ };
+ setCanSave(checkEmailConfigurationValidity(newConfig as EmailConfiguration));
+ setNewConfiguration(newConfig as EmailConfiguration);
+ },
+ [newConfiguration, setNewConfiguration],
+ );
+
+ const onSubmit = useCallback(
+ (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ if (canSave && newConfiguration) {
+ const authConfiguration =
+ newConfiguration.authMethod === AuthMethod.OAuth
+ ? {
+ oauthAuthenticationHost: newConfiguration.oauthAuthenticationHost,
+ oauthClientId: newConfiguration.oauthClientId,
+ oauthClientSecret: newConfiguration.oauthClientSecret,
+ oauthTenant: newConfiguration.oauthTenant,
+ }
+ : {
+ basicPassword: newConfiguration.basicPassword,
+ };
+
+ const newEmailConfiguration = {
+ ...newConfiguration,
+ ...authConfiguration,
+ };
+
+ if (newConfiguration?.id === undefined) {
+ saveEmailConfiguration(newEmailConfiguration);
+ } else {
+ newEmailConfiguration.id = newConfiguration.id;
+ updateEmailConfiguration({
+ emailConfiguration: newEmailConfiguration,
+ id: newEmailConfiguration.id,
+ });
+ }
+ }
+ },
+ [canSave, newConfiguration, saveEmailConfiguration, updateEmailConfiguration],
+ );
+
+ useEffect(() => {
+ if (emailConfiguration !== null) {
+ setNewConfiguration(emailConfiguration);
+ }
+ }, [emailConfiguration]);
+
+ return (
+ <form id={FORM_ID} onSubmit={onSubmit}>
+ <NumberedList>
+ <NumberedListItem className="sw-pt-6">
+ <span className="sw-body-sm-highlight">
+ {translate('email_notification.subheading.1')}
+ </span>
+ <AuthenticationSelector configuration={newConfiguration} onChange={onChange} />
+ </NumberedListItem>
+ <NumberedListItem className="sw-pt-6">
+ <span className="sw-body-sm-highlight">
+ {translate('email_notification.subheading.2')}
+ </span>
+ <CommonSMTP configuration={newConfiguration} onChange={onChange} />
+ </NumberedListItem>
+ <NumberedListItem className="sw-pt-6">
+ <span className="sw-body-sm-highlight">
+ {translate('email_notification.subheading.3')}
+ </span>
+ <SenderInformation configuration={newConfiguration} onChange={onChange} />
+ </NumberedListItem>
+ </NumberedList>
+ <Button
+ className="sw-ml-4"
+ isDisabled={!canSave}
+ type="submit"
+ variety={ButtonVariety.Primary}
+ >
+ {translate('email_notification.form.save_configuration')}
+ </Button>
+ </form>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { InputSize, Select } from '@sonarsource/echoes-react';
+import { FormField, InputField, TextError } from 'design-system/lib';
+import { isEmpty, isUndefined } from 'lodash';
+import React from 'react';
+import isEmail from 'validator/lib/isEmail';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+
+type InputType = 'email' | 'number' | 'password' | 'select' | 'text';
+
+interface Props {
+ children?: (props: { onChange: (value: string) => void }) => React.ReactNode;
+ description: string;
+ id: string;
+ name: string;
+ onChange: (value: string) => void;
+ options?: string[];
+ required?: boolean;
+ type?: InputType;
+ value: string | undefined;
+}
+
+export function EmailNotificationFormField(props: Readonly<Props>) {
+ const { description, id, name, options, required, type = 'text', value } = props;
+
+ const [validationMessage, setValidationMessage] = React.useState<string>();
+
+ const handleCheck = (changedValue?: string) => {
+ if (isEmpty(changedValue) && required) {
+ setValidationMessage(translate('settings.state.value_cant_be_empty_no_default'));
+ return false;
+ }
+
+ if (type === 'email' && !isEmail(changedValue ?? '')) {
+ setValidationMessage(translate('email_notification.state.value_should_be_valid_email'));
+ return false;
+ }
+
+ setValidationMessage(undefined);
+ return true;
+ };
+
+ const onChange = (newValue: string) => {
+ handleCheck(newValue);
+ props.onChange(newValue);
+ };
+
+ const hasValidationMessage = !isUndefined(validationMessage);
+
+ return (
+ <FormField
+ className="sw-grid sw-grid-cols-2 sw-gap-x-4 sw-py-6 sw-px-4"
+ htmlFor={id}
+ label={translate(name)}
+ required={required}
+ requiredAriaLabel={translate('field_required')}
+ >
+ <div className="sw-row-span-2 sw-grid">
+ {type === 'select' ? (
+ <SelectInput
+ id={id}
+ name={name}
+ options={options ?? []}
+ onChange={onChange}
+ required={required}
+ value={value}
+ />
+ ) : (
+ <BasicInput
+ id={id}
+ name={name}
+ type={type}
+ onChange={onChange}
+ required={required}
+ value={value}
+ />
+ )}
+
+ {hasValidationMessage && (
+ <TextError
+ className="sw-mt-2"
+ text={translateWithParameters('settings.state.validation_failed', validationMessage)}
+ />
+ )}
+ </div>
+
+ <div className="sw-w-abs-300">
+ {!isUndefined(description) && <div className="markdown sw-mt-1">{description}</div>}
+ </div>
+ </FormField>
+ );
+}
+
+function BasicInput(
+ props: Readonly<{
+ id: string;
+ name: string;
+ onChange: (value: string) => void;
+ required?: boolean;
+ type: InputType;
+ value: string | undefined;
+ }>,
+) {
+ const { id, onChange, name, required, type, value } = props;
+
+ return (
+ <InputField
+ id={id}
+ min={type === 'number' ? 0 : undefined}
+ name={name}
+ onChange={(event) => onChange(event.target.value)}
+ required={required}
+ size="large"
+ step={type === 'number' ? 1 : undefined}
+ type={type}
+ value={value ?? ''}
+ />
+ );
+}
+
+function SelectInput(
+ props: Readonly<{
+ id: string;
+ name: string;
+ onChange: (value: string) => void;
+ options: string[];
+ required?: boolean;
+ value: string | undefined;
+ }>,
+) {
+ const { id, name, onChange, options, required, value } = props;
+
+ return (
+ <Select
+ data={options?.map((option) => ({ label: option, value: option })) ?? []}
+ id={id}
+ isNotClearable
+ isRequired={required}
+ name={name}
+ onChange={onChange}
+ size={InputSize.Large}
+ value={value}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { BasicSeparator } from 'design-system/lib';
+import React from 'react';
+import { translate } from '../../../../helpers/l10n';
+import { EmailNotificationFormField } from './EmailNotificationFormField';
+import { EmailNotificationGroupProps, FROM_ADDRESS, FROM_NAME, SUBJECT_PREFIX } from './utils';
+
+export function SenderInformation(props: Readonly<EmailNotificationGroupProps>) {
+ const { configuration, onChange } = props;
+
+ return (
+ <div className="sw-pt-6">
+ <EmailNotificationFormField
+ description={translate('email_notification.form.from_address.description')}
+ id={FROM_ADDRESS}
+ name={translate('email_notification.form.from_address')}
+ onChange={(value) => onChange({ fromAddress: value })}
+ required
+ type="email"
+ value={configuration.fromAddress}
+ />
+ <BasicSeparator />
+ <EmailNotificationFormField
+ description={translate('email_notification.form.from_name.description')}
+ id={FROM_NAME}
+ onChange={(value) => onChange({ fromName: value })}
+ name={translate('email_notification.form.from_name')}
+ required
+ value={configuration.fromName}
+ />
+ <BasicSeparator />
+ <EmailNotificationFormField
+ description={translate('email_notification.form.subject_prefix.description')}
+ id={SUBJECT_PREFIX}
+ onChange={(value) => onChange({ subjectPrefix: value })}
+ name={translate('email_notification.form.subject_prefix')}
+ required
+ value={configuration.subjectPrefix}
+ />
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { addGlobalSuccessMessage } from 'design-system/lib';
+import React from 'react';
+import { byLabelText, byRole, byText } from '~sonar-aligned/helpers/testSelector';
+import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
+import * as api from '../../../../../api/system';
+import { mockEmailConfiguration } from '../../../../../helpers/mocks/system';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { AuthMethod } from '../../../../../types/system';
+import EmailNotification from '../EmailNotification';
+
+jest.mock('../../../../../api/system');
+jest.mock('../../../../../api/settings');
+
+jest.mock('design-system', () => ({
+ ...jest.requireActual('design-system'),
+ addGlobalSuccessMessage: jest.fn(),
+}));
+
+const systemHandler = new SystemServiceMock();
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ systemHandler.reset();
+});
+
+const ui = {
+ editSubheading1: byText('email_notification.subheading.1'),
+
+ // common fields
+ selectorBasicAuth: byRole('radio', {
+ name: 'email_notification.form.basic_auth.title email_notification.form.basic_auth.description',
+ }),
+ selectorOAuthAuth: byRole('radio', {
+ name: 'email_notification.form.oauth_auth.title email_notification.form.oauth_auth.description recommended email_notification.form.oauth_auth.recommended_reason',
+ }),
+ host: byRole('textbox', {
+ name: 'email_notification.form.host field_required',
+ }),
+ port: byRole('spinbutton', {
+ name: 'email_notification.form.port field_required',
+ }),
+ securityProtocol: byRole('searchbox', {
+ name: 'email_notification.form.security_protocol field_required',
+ }),
+ fromAddress: byRole('textbox', {
+ name: 'email_notification.form.from_address field_required',
+ }),
+ fromName: byRole('textbox', {
+ name: 'email_notification.form.from_name field_required',
+ }),
+ subjectPrefix: byRole('textbox', {
+ name: 'email_notification.form.subject_prefix field_required',
+ }),
+ username: byRole('textbox', {
+ name: 'email_notification.form.username field_required',
+ }),
+
+ // basic authentication
+ basic_password: byLabelText('email_notification.form.basic_password*'),
+
+ // oauth
+ oauth_auth_host: byRole('textbox', {
+ name: 'email_notification.form.oauth_authentication_host field_required',
+ }),
+ oauth_client_id: byLabelText('email_notification.form.oauth_client_id*'),
+ oauth_client_secret: byLabelText('email_notification.form.oauth_client_secret*'),
+ oauth_tenant: byRole('textbox', { name: 'email_notification.form.oauth_tenant field_required' }),
+
+ save: byRole('button', {
+ name: 'email_notification.form.save_configuration',
+ }),
+};
+
+describe('Email Basic Configuration', () => {
+ it('can save the basic configuration', async () => {
+ jest.spyOn(api, 'postEmailConfiguration');
+ const user = userEvent.setup();
+ renderEmailNotifications();
+ expect(await ui.editSubheading1.find()).toBeInTheDocument();
+
+ expect(ui.save.get()).toBeDisabled();
+
+ expect(ui.selectorBasicAuth.get()).toBeChecked();
+ expect(ui.username.get()).toHaveValue('');
+ expect(ui.basic_password.get()).toHaveValue('');
+ expect(ui.host.get()).toHaveValue('');
+ expect(ui.port.get()).toHaveValue(587);
+ expect(ui.securityProtocol.get()).toHaveValue('');
+ expect(ui.fromAddress.get()).toHaveValue('');
+ expect(ui.fromName.get()).toHaveValue('SonarQube');
+ expect(ui.subjectPrefix.get()).toHaveValue('[SonarQube]');
+
+ await user.type(ui.basic_password.get(), 'password');
+ await user.type(ui.host.get(), 'host');
+ await user.clear(ui.port.get());
+ await user.type(ui.port.get(), '1234');
+ await user.click(ui.securityProtocol.get());
+ await user.click(screen.getByText('SSLTLS'));
+ await user.type(ui.fromAddress.get(), 'admin@localhost.com');
+ await user.clear(ui.fromName.get());
+ await user.type(ui.fromName.get(), 'fromName');
+ await user.clear(ui.subjectPrefix.get());
+ await user.type(ui.subjectPrefix.get(), 'prefix');
+ await user.type(ui.username.get(), 'username');
+
+ expect(ui.selectorBasicAuth.get()).toBeChecked();
+ expect(ui.username.get()).toHaveValue('username');
+ expect(ui.basic_password.get()).toHaveValue('password');
+ expect(ui.host.get()).toHaveValue('host');
+ expect(ui.port.get()).toHaveValue(1234);
+ expect(ui.securityProtocol.get()).toHaveValue('SSLTLS');
+ expect(ui.fromAddress.get()).toHaveValue('admin@localhost.com');
+ expect(ui.fromName.get()).toHaveValue('fromName');
+ expect(ui.subjectPrefix.get()).toHaveValue('prefix');
+
+ expect(await ui.save.find()).toBeEnabled();
+ await user.click(ui.save.get());
+
+ expect(api.postEmailConfiguration).toHaveBeenCalledTimes(1);
+ expect(api.postEmailConfiguration).toHaveBeenCalledWith({
+ authMethod: 'BASIC',
+ basicPassword: 'password',
+ fromAddress: 'admin@localhost.com',
+ fromName: 'fromName',
+ host: 'host',
+ port: '1234',
+ securityProtocol: 'SSLTLS',
+ subjectPrefix: 'prefix',
+ username: 'username',
+ });
+
+ expect(addGlobalSuccessMessage).toHaveBeenCalledWith(
+ 'email_notification.form.save_configuration.create_success',
+ );
+ });
+
+ it('can edit an existing configuration', async () => {
+ systemHandler.addEmailConfiguration(
+ mockEmailConfiguration(AuthMethod.Basic, { id: 'email-1' }),
+ );
+ jest.spyOn(api, 'patchEmailConfiguration');
+ const user = userEvent.setup();
+ renderEmailNotifications();
+ expect(await ui.editSubheading1.find()).toBeInTheDocument();
+
+ expect(ui.save.get()).toBeDisabled();
+ await user.type(ui.basic_password.get(), 'updated');
+ await user.type(ui.host.get(), '-updated');
+ await user.type(ui.port.get(), '5678');
+ await user.click(ui.securityProtocol.get());
+ await user.click(screen.getByText('STARTTLS'));
+ await user.clear(ui.fromAddress.get());
+ await user.type(ui.fromAddress.get(), 'updated@email.com');
+ await user.type(ui.fromName.get(), '-updated');
+ await user.type(ui.subjectPrefix.get(), '-updated');
+ await user.type(ui.username.get(), '-updated');
+
+ expect(await ui.save.find()).toBeEnabled();
+ await user.click(ui.save.get());
+
+ expect(api.patchEmailConfiguration).toHaveBeenCalledTimes(1);
+ expect(api.patchEmailConfiguration).toHaveBeenCalledWith('email-1', {
+ authMethod: 'BASIC',
+ basicPassword: 'updated',
+ fromAddress: 'updated@email.com',
+ fromName: 'from_name-updated',
+ host: 'host-updated',
+ id: 'email-1',
+ isBasicPasswordSet: true,
+ port: '5678',
+ securityProtocol: 'STARTTLS',
+ subjectPrefix: 'subject_prefix-updated',
+ username: 'username-updated',
+ });
+
+ expect(addGlobalSuccessMessage).toHaveBeenCalledWith(
+ 'email_notification.form.save_configuration.update_success',
+ );
+ });
+});
+
+describe('Email Oauth Configuration', () => {
+ it('can save the oauth configuration', async () => {
+ jest.spyOn(api, 'postEmailConfiguration');
+ const user = userEvent.setup();
+ renderEmailNotifications();
+ expect(await ui.editSubheading1.find()).toBeInTheDocument();
+ await user.click(ui.selectorOAuthAuth.get());
+
+ expect(ui.save.get()).toBeDisabled();
+
+ expect(ui.selectorOAuthAuth.get()).toBeChecked();
+ expect(ui.oauth_auth_host.get()).toHaveValue('');
+ expect(ui.oauth_client_id.get()).toHaveValue('');
+ expect(ui.oauth_client_secret.get()).toHaveValue('');
+ expect(ui.oauth_tenant.get()).toHaveValue('');
+ expect(ui.host.get()).toHaveValue('');
+ expect(ui.port.get()).toHaveValue(587);
+ expect(ui.securityProtocol.get()).toHaveValue('');
+ expect(ui.fromAddress.get()).toHaveValue('');
+ expect(ui.fromName.get()).toHaveValue('SonarQube');
+ expect(ui.subjectPrefix.get()).toHaveValue('[SonarQube]');
+
+ await user.type(ui.oauth_auth_host.get(), 'oauth_auth_host');
+ await user.type(ui.oauth_client_id.get(), 'oauth_client_id');
+ await user.type(ui.oauth_client_secret.get(), 'oauth_client_secret');
+ await user.type(ui.oauth_tenant.get(), 'oauth_tenant');
+ await user.type(ui.host.get(), 'host');
+ await user.clear(ui.port.get());
+ await user.type(ui.port.get(), '1234');
+ await user.click(ui.securityProtocol.get());
+ await user.click(screen.getByText('SSLTLS'));
+ await user.type(ui.fromAddress.get(), 'admin@localhost.com');
+ await user.clear(ui.fromName.get());
+ await user.type(ui.fromName.get(), 'fromName');
+ await user.clear(ui.subjectPrefix.get());
+ await user.type(ui.subjectPrefix.get(), 'prefix');
+ await user.type(ui.username.get(), 'username');
+
+ expect(ui.selectorOAuthAuth.get()).toBeChecked();
+ expect(ui.username.get()).toHaveValue('username');
+ expect(ui.oauth_auth_host.get()).toHaveValue('oauth_auth_host');
+ expect(ui.oauth_client_id.get()).toHaveValue('oauth_client_id');
+ expect(ui.oauth_client_secret.get()).toHaveValue('oauth_client_secret');
+ expect(ui.oauth_tenant.get()).toHaveValue('oauth_tenant');
+ expect(ui.host.get()).toHaveValue('host');
+ expect(ui.port.get()).toHaveValue(1234);
+ expect(ui.securityProtocol.get()).toHaveValue('SSLTLS');
+ expect(ui.fromAddress.get()).toHaveValue('admin@localhost.com');
+ expect(ui.fromName.get()).toHaveValue('fromName');
+ expect(ui.subjectPrefix.get()).toHaveValue('prefix');
+
+ expect(await ui.save.find()).toBeEnabled();
+ await user.click(ui.save.get());
+
+ expect(api.postEmailConfiguration).toHaveBeenCalledTimes(1);
+ expect(api.postEmailConfiguration).toHaveBeenCalledWith({
+ authMethod: 'OAUTH',
+ basicPassword: '',
+ oauthAuthenticationHost: 'oauth_auth_host',
+ oauthClientId: 'oauth_client_id',
+ oauthClientSecret: 'oauth_client_secret',
+ oauthTenant: 'oauth_tenant',
+ fromAddress: 'admin@localhost.com',
+ fromName: 'fromName',
+ host: 'host',
+ port: '1234',
+ securityProtocol: 'SSLTLS',
+ subjectPrefix: 'prefix',
+ username: 'username',
+ });
+
+ expect(addGlobalSuccessMessage).toHaveBeenCalledWith(
+ 'email_notification.form.save_configuration.create_success',
+ );
+ });
+
+ it('can edit the oauth configuration', async () => {
+ systemHandler.addEmailConfiguration(
+ mockEmailConfiguration(AuthMethod.OAuth, { id: 'email-1' }),
+ );
+ jest.spyOn(api, 'patchEmailConfiguration');
+ const user = userEvent.setup();
+ renderEmailNotifications();
+ expect(await ui.editSubheading1.find()).toBeInTheDocument();
+ await user.click(ui.selectorOAuthAuth.get());
+
+ expect(ui.save.get()).toBeDisabled();
+ await user.type(ui.oauth_auth_host.get(), '-updated');
+ await user.type(ui.oauth_client_id.get(), 'updated_id');
+ await user.type(ui.oauth_client_secret.get(), 'updated_secret');
+ await user.type(ui.oauth_tenant.get(), '-updated');
+ await user.type(ui.host.get(), '-updated');
+ await user.type(ui.port.get(), '5678');
+ await user.click(ui.securityProtocol.get());
+ await user.click(screen.getByText('STARTTLS'));
+ await user.clear(ui.fromAddress.get());
+ await user.type(ui.fromAddress.get(), 'updated@email.com');
+ await user.type(ui.fromName.get(), '-updated');
+ await user.type(ui.subjectPrefix.get(), '-updated');
+ await user.type(ui.username.get(), '-updated');
+
+ expect(await ui.save.find()).toBeEnabled();
+ await user.click(ui.save.get());
+
+ expect(api.patchEmailConfiguration).toHaveBeenCalledTimes(1);
+ expect(api.patchEmailConfiguration).toHaveBeenCalledWith('email-1', {
+ authMethod: 'OAUTH',
+ oauthAuthenticationHost: 'oauth_auth_host-updated',
+ oauthClientId: 'updated_id',
+ oauthClientSecret: 'updated_secret',
+ oauthTenant: 'oauth_tenant-updated',
+ fromAddress: 'updated@email.com',
+ fromName: 'from_name-updated',
+ host: 'host-updated',
+ id: 'email-1',
+ isOauthClientIdSet: true,
+ isOauthClientSecretSet: true,
+ port: '5678',
+ securityProtocol: 'STARTTLS',
+ subjectPrefix: 'subject_prefix-updated',
+ username: 'username-updated',
+ });
+
+ expect(addGlobalSuccessMessage).toHaveBeenCalledWith(
+ 'email_notification.form.save_configuration.update_success',
+ );
+ });
+});
+
+function renderEmailNotifications() {
+ return renderComponent(<EmailNotification />);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { isUndefined } from 'lodash';
+import { AuthMethod, EmailConfiguration } from '../../../../types/system';
+
+export const AUTH_METHOD = 'auth-method';
+
+// Basic Authentication
+export const BASIC_PASSWORD = 'basic-password';
+export const IS_BASIC_PASSWORD_SET = 'is-basic-password-set';
+
+// OAuth Authentication
+export const IS_OAUTH_CLIENT_ID_SET = 'is-oauth-client-id-set';
+export const IS_OAUTH_CLIENT_SECRET_SET = 'is-oauth-client-secret-set';
+export const OAUTH_AUTHENTICATION_HOST = 'oauth-authentication-host';
+export const OAUTH_CLIENT_ID = 'oauth-client-id';
+export const OAUTH_CLIENT_SECRET = 'oauth-client-secret';
+export const OAUTH_TENANT = 'oauth-tenant';
+
+// Common settings
+export const USERNAME = 'username';
+export const HOST = 'host';
+export const PORT = 'port';
+export const SECURITY_PROTOCOL = 'security-protocol';
+export const FROM_ADDRESS = 'from-address';
+export const FROM_NAME = 'from-mame';
+export const SUBJECT_PREFIX = 'subject-prefix';
+
+export interface EmailNotificationGroupProps {
+ configuration: EmailConfiguration;
+ onChange: (newValue: Partial<EmailConfiguration>) => void;
+}
+
+export function checkEmailConfigurationValidity(configuration: EmailConfiguration): boolean {
+ let isValid = false;
+ const commonProps: (keyof EmailConfiguration)[] = [
+ 'authMethod',
+ 'username',
+ 'host',
+ 'port',
+ 'securityProtocol',
+ 'fromAddress',
+ 'fromName',
+ 'subjectPrefix',
+ ];
+
+ if (configuration.authMethod === AuthMethod.Basic) {
+ isValid = checkRequiredPropsAreValid(configuration, [...commonProps, 'basicPassword']);
+ } else {
+ isValid = checkRequiredPropsAreValid(configuration, [
+ ...commonProps,
+ 'oauthAuthenticationHost',
+ 'oauthClientId',
+ 'oauthClientSecret',
+ 'oauthTenant',
+ ]);
+ }
+
+ return isValid;
+}
+
+function checkRequiredPropsAreValid<T>(obj: T, props: (keyof T)[]): boolean {
+ return props.every(
+ (prop) => !isUndefined(obj[prop]) && typeof obj[prop] === 'string' && obj[prop].length > 0,
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { AuthMethod, EmailConfiguration } from '../../types/system';
+
+export function mockEmailConfiguration(
+ authMethod: AuthMethod,
+ overrides: Partial<EmailConfiguration> = {},
+): EmailConfiguration {
+ const base: Partial<EmailConfiguration> = {
+ fromAddress: 'from_address',
+ fromName: 'from_name',
+ host: 'host',
+ id: '1',
+ port: 'port',
+ subjectPrefix: 'subject_prefix',
+ securityProtocol: 'SSLTLS',
+ username: 'username',
+ };
+
+ const mock =
+ authMethod === AuthMethod.Basic
+ ? {
+ ...base,
+ authMethod: AuthMethod.Basic,
+ basicPassword: undefined,
+ isBasicPasswordSet: true,
+ }
+ : {
+ ...base,
+ authMethod: AuthMethod.OAuth,
+ isOauthClientIdSet: true,
+ isOauthClientSecretSet: true,
+ oauthAuthenticationHost: 'oauth_auth_host',
+ oauthClientId: undefined,
+ oauthClientSecret: undefined,
+ oauthTenant: 'oauth_tenant',
+ };
+
+ return { ...mock, ...overrides } as EmailConfiguration;
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { queryOptions } from '@tanstack/react-query';
-import { getSystemUpgrades } from '../api/system';
+import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { addGlobalSuccessMessage } from 'design-system/lib';
+import {
+ getEmailConfigurations,
+ getSystemUpgrades,
+ patchEmailConfiguration,
+ postEmailConfiguration,
+} from '../api/system';
+import { translate } from '../helpers/l10n';
+import { EmailConfiguration } from '../types/system';
import { createQueryHook } from './common';
export const useSystemUpgrades = createQueryHook(() => {
staleTime: Infinity,
});
});
+
+export function useGetEmailConfiguration() {
+ return useQuery({
+ queryKey: ['email_configuration'] as const,
+ queryFn: async () => {
+ const { emailConfigurations } = await getEmailConfigurations();
+ return emailConfigurations && emailConfigurations.length > 0 ? emailConfigurations[0] : null;
+ },
+ });
+}
+
+export function useSaveEmailConfigurationMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: EmailConfiguration) => {
+ return postEmailConfiguration(data);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['email_configuration'] });
+ addGlobalSuccessMessage(
+ translate('email_notification.form.save_configuration.create_success'),
+ );
+ },
+ });
+}
+
+export function useUpdateEmailConfigurationMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({
+ emailConfiguration,
+ id,
+ }: {
+ emailConfiguration: EmailConfiguration;
+ id: string;
+ }) => {
+ return patchEmailConfiguration(id, emailConfiguration);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['email_configuration'] });
+ addGlobalSuccessMessage(
+ translate('email_notification.form.save_configuration.update_success'),
+ );
+ },
+ });
+}
status: MigrationStatus;
totalSteps?: number;
}
+
+export enum AuthMethod {
+ Basic = 'BASIC',
+ OAuth = 'OAUTH',
+}
+
+export type EmailConfiguration = (
+ | {
+ authMethod: AuthMethod.Basic;
+ basicPassword: string;
+ readonly isBasicPasswordSet?: boolean;
+ }
+ | {
+ authMethod: AuthMethod.OAuth;
+ readonly isOauthClientIdSet?: boolean;
+ readonly isOauthClientSecretSet?: boolean;
+ oauthAuthenticationHost: string;
+ oauthClientId: string;
+ oauthClientSecret: string;
+ oauthTenant: string;
+ }
+) &
+ EmailConfigurationCommon;
+
+interface EmailConfigurationCommon {
+ fromAddress: string;
+ fromName: string;
+ host: string;
+ id?: string;
+ port: string;
+ securityProtocol: string;
+ subjectPrefix: string;
+ username: string;
+}
languageName: node
linkType: hard
+"@types/validator@npm:13.12.0":
+ version: 13.12.0
+ resolution: "@types/validator@npm:13.12.0"
+ checksum: 10/b3344ef630ff9a3ffab4ce10da268e7be98ca2df9cbd956fb5cac860bd661c7ff6e82e0cdc7b253f037a98cf3b233fff3d04d28330bcd3ca2cafb0c52253976e
+ languageName: node
+ linkType: hard
+
"@types/yargs-parser@npm:*":
version: 15.0.0
resolution: "@types/yargs-parser@npm:15.0.0"
"@types/react-modal": "npm:3.16.3"
"@types/react-virtualized": "npm:9.21.30"
"@types/valid-url": "npm:1.0.7"
+ "@types/validator": "npm:13.12.0"
"@typescript-eslint/eslint-plugin": "npm:6.21.0"
"@typescript-eslint/parser": "npm:6.21.0"
"@typescript-eslint/rule-tester": "npm:6.21.0"
turbo: "npm:1.11.3"
typescript: "npm:5.5.3"
valid-url: "npm:1.0.9"
+ validator: "npm:13.12.0"
whatwg-fetch: "npm:3.6.20"
languageName: unknown
linkType: soft
languageName: node
linkType: hard
+"validator@npm:13.12.0":
+ version: 13.12.0
+ resolution: "validator@npm:13.12.0"
+ checksum: 10/db6eb0725e2b67d60d30073ae8573982713b5903195d031dc3c7db7e82df8b74e8c13baef8e2106d146d979599fd61a06cde1fec5c148e4abd53d52817ff0fd9
+ languageName: node
+ linkType: hard
+
"validator@npm:^13.7.0":
version: 13.9.0
resolution: "validator@npm:13.9.0"
#------------------------------------------------------------------------------
#
-# EMAIL CONFIGURATION
-#
-#------------------------------------------------------------------------------
+# EMAIL NOTIFICATION CONFIGURATION
+#
+#------------------------------------------------------------------------------
+email_notification.category=Email Notification
+email_notification.header=SMTP Configuration
+email_notification.description=Follow the steps below to configure and test your authentication.
+email_notification.subheading.1=Select your authentication type and complete the fields below
+email_notification.subheading.2=Complete the SMTP fields below
+email_notification.subheading.3=Input the ‘sender’ information below to identify your email notification
+email_notification.form.basic_auth.title=Basic Authentication
+email_notification.form.basic_auth.description=Authenticate with a username and password
+email_notification.form.username=SMTP username
+email_notification.form.username.description=Username used to authenticate to the SMTP server.
+email_notification.form.basic_password=SMTP password
+email_notification.form.basic_password.description=Password used to authenticate to the SMTP server.
+email_notification.form.oauth_auth.title=Modern Authentication
+email_notification.form.oauth_auth.description=Authenticate with OAuth Microsoft
+email_notification.form.oauth_auth.recommended_reason=for stronger security compliance
+email_notification.form.oauth_authentication_host=Authentication host
+email_notification.form.oauth_authentication_host.description=Host of the Identity Provider issuing access tokens.
+email_notification.form.oauth_client_id=Client ID
+email_notification.form.oauth_client_id.description=Client ID provided by Microsoft Exchange when registering the application.
+email_notification.form.oauth_client_secret=Client Secret
+email_notification.form.oauth_client_secret.description=Client password provided by Microsoft Exchange when registering the application.
+email_notification.form.oauth_tenant=Tenant
+email_notification.form.oauth_tenant.description=Microsoft tenant.
+email_notification.form.host=SMTP host
+email_notification.form.host.description=URL of your SMTP server.
+email_notification.form.port=SMTP port
+email_notification.form.port.description=Port of your SMTP server (usually 25, 587 or 465).
+email_notification.form.security_protocol=Security protocol
+email_notification.form.security_protocol.description=Security protocol used to connect to your SMTP server (SSLTLS is recommended). Valid values: NONE, SSLTLS, STARTTLS.
+email_notification.form.from_address=From address
+email_notification.form.from_address.description=Address emails will come from.
+email_notification.form.from_name=From name
+email_notification.form.from_name.description=Name emails will come from (usually "SonarQube").
+email_notification.form.subject_prefix=Subject prefix
+email_notification.form.subject_prefix.description=Prefix added to email so they can be easily recognized (usually "[SonarQube]").
+email_notification.form.save_configuration=Save configuration
+email_notification.form.save_configuration.create_success=Email configuration saved successfully.
+email_notification.form.save_configuration.update_success=Email configuration updated successfully.
+email_notification.form.delete_configuration=Delete configuration
+email_notification.state.value_should_be_valid_email=A valid email address is required.
+
email_configuration.test.title=Test Configuration
email_configuration.test.to_address=To
email_configuration.test.subject=Subject