diff --git a/app/localization/translated/be.json b/app/localization/translated/be.json index cb64cae3cd..f449811b43 100644 --- a/app/localization/translated/be.json +++ b/app/localization/translated/be.json @@ -647,7 +647,8 @@ "EditWidgetModal.editWidgetSuccess": "Віджэт абноўлены!", "EditWidgetModal.headerText": "Рэдагаваць віджэт", "EmailFormFields.authLabel": "Аўтарызацыя", - "EmailFormFields.fromLabel": "Імя адпраўніка па змаўчанні", + "EmailFormFields.fromEmailLabel": "From Email", + "EmailFormFields.fromNameLabel": "From name", "EmailFormFields.hostLabel": "Хост", "EmailFormFields.passwordLabel": "Пароль", "EmailFormFields.portFieldHint": "Магчымы толькі лічбы ад '1' да '65535'", @@ -1944,6 +1945,10 @@ "SortingControl.sortByFailedItems": "Няўдалыя пункты", "SortingControl.sortByPassingRate": "Прахадны бал", "SortingControl.sortByTotal": "Агульная колькасць", + "SsoUsersForm.formHeader": "Instance Invitations", + "SsoUsersForm.manualInvitesDescription": "Карыстальнікі могуць адпраўляць запрашэнні іншым карыстальнікам. Калі ўключана, новыя карыстальнікі могуць быць ствараны толькі праз SSO.", + "SsoUsersForm.ssoOnlyDescription": "Новыя карыстальнікі могуць быць створаны толькі праз SSO.", + "SsoUsersForm.switcherLabel": "Толькі SSO карыстальнікі", "StackTrace.jumpTo": "Перайсці", "StackTrace.linkText": "Адкрыць логі", "StackTrace.loadLabel": "Загрузіць яшчэ", diff --git a/app/localization/translated/es.json b/app/localization/translated/es.json index 4120bddd30..6f5adf1795 100644 --- a/app/localization/translated/es.json +++ b/app/localization/translated/es.json @@ -646,7 +646,8 @@ "EditWidgetModal.editWidgetSuccess": "¡Widget actualizado!", "EditWidgetModal.headerText": "Editar widget", "EmailFormFields.authLabel": "Autorización", - "EmailFormFields.fromLabel": "Nombre del remitente predeterminado", + "EmailFormFields.fromEmailLabel": "From Email", + "EmailFormFields.fromNameLabel": "From name", "EmailFormFields.hostLabel": "Host", "EmailFormFields.passwordLabel": "Contraseña", "EmailFormFields.portFieldHint": "Solo se permiten números del '1' al '65535'", @@ -1922,6 +1923,10 @@ "SortingControl.sortByFailedItems": "Elementos fallidos", "SortingControl.sortByPassingRate": "Porcentaje de aprobados", "SortingControl.sortByTotal": "Cantidad total", + "SsoUsersForm.formHeader": "Instance Invitations", + "SsoUsersForm.manualInvitesDescription": "Users can manually send invitations for other users. If enabled new users can be created via SSO only.", + "SsoUsersForm.ssoOnlyDescription": "New users can be created via SSO only.", + "SsoUsersForm.switcherLabel": "SSO users only", "StackTrace.jumpTo": "Ir a", "StackTrace.linkText": "Abrir registros", "StackTrace.loadLabel": "Cargar más", diff --git a/app/localization/translated/ru.json b/app/localization/translated/ru.json index b31c24fe92..d003a9d17b 100644 --- a/app/localization/translated/ru.json +++ b/app/localization/translated/ru.json @@ -647,7 +647,8 @@ "EditWidgetModal.editWidgetSuccess": "Виджет обновлен!", "EditWidgetModal.headerText": "Редактировать виджет", "EmailFormFields.authLabel": "Авторизация", - "EmailFormFields.fromLabel": "Имя отправителя по умолчанию", + "EmailFormFields.fromEmailLabel": "From Email", + "EmailFormFields.fromNameLabel": "From name", "EmailFormFields.hostLabel": "Хост", "EmailFormFields.passwordLabel": "Пароль", "EmailFormFields.portFieldHint": "Возможны только цифры от '1' до '65535'", @@ -1941,6 +1942,10 @@ "SortingControl.sortByFailedItems": "Неудачные пункты", "SortingControl.sortByPassingRate": "Проходной балл", "SortingControl.sortByTotal": "Общее количество", + "SsoUsersForm.formHeader": "Instance Invitations", + "SsoUsersForm.manualInvitesDescription": "Пользователи могут вручную отправлять приглашения другим пользователям. Если включено, новых пользователей можно создавать только через SSO.", + "SsoUsersForm.ssoOnlyDescription": "Новых пользователей можно создавать только через SSO.", + "SsoUsersForm.switcherLabel": "Только SSO пользователи", "StackTrace.jumpTo": "Перейти", "StackTrace.linkText": "Открыть логи", "StackTrace.loadLabel": "Загрузить еще", diff --git a/app/localization/translated/uk.json b/app/localization/translated/uk.json index 4faee0f739..6a55d43530 100644 --- a/app/localization/translated/uk.json +++ b/app/localization/translated/uk.json @@ -647,7 +647,8 @@ "EditWidgetModal.editWidgetSuccess": "Оновлений Віджет!", "EditWidgetModal.headerText": "Віджет Редагувати", "EmailFormFields.authLabel": "Авторизація", - "EmailFormFields.fromLabel": "Ім’я відправника за замовчуванням", + "EmailFormFields.fromEmailLabel": "From Email", + "EmailFormFields.fromNameLabel": "From name", "EmailFormFields.hostLabel": "Хост", "EmailFormFields.passwordLabel": "Пароль", "EmailFormFields.portFieldHint": "Можливі тільки цифри від '1' до '65535'", @@ -1943,6 +1944,10 @@ "SortingControl.sortByFailedItems": "Невдалі пункти", "SortingControl.sortByPassingRate": "Прохідний бал", "SortingControl.sortByTotal": "Загальна кількість", + "SsoUsersForm.formHeader": "Instance Invitations", + "SsoUsersForm.manualInvitesDescription": "Користувачі можуть самостійно надсилати запрошення іншим користувачам. Якщо ввімкнено, нові користувачі створюються виключно через SSO.", + "SsoUsersForm.ssoOnlyDescription": "Користувачі створюються виключно через SSO.", + "SsoUsersForm.switcherLabel": "SSO users only", "StackTrace.jumpTo": "Перейти", "StackTrace.linkText": "Логи Відкрити", "StackTrace.loadLabel": "Завантажити", diff --git a/app/localization/translated/zh.json b/app/localization/translated/zh.json index f7e5cd9413..e70470bdf9 100644 --- a/app/localization/translated/zh.json +++ b/app/localization/translated/zh.json @@ -647,7 +647,8 @@ "EditWidgetModal.editWidgetSuccess": "小部件已更新", "EditWidgetModal.headerText": "编辑小部件", "EmailFormFields.authLabel": "授权", - "EmailFormFields.fromLabel": "默认发件人姓名", + "EmailFormFields.fromEmailLabel": "From Email", + "EmailFormFields.fromNameLabel": "From name", "EmailFormFields.hostLabel": "服务器(Host)", "EmailFormFields.passwordLabel": "密码", "EmailFormFields.portFieldHint": "只允许输入从“1”到“65535”的数字", @@ -1943,6 +1944,10 @@ "SortingControl.sortByFailedItems": "失败的测试项", "SortingControl.sortByPassingRate": "通过率", "SortingControl.sortByTotal": "全部", + "SsoUsersForm.formHeader": "Instance Invitations", + "SsoUsersForm.manualInvitesDescription": "Users can manually send invitations for other users. If enabled new users can be created via SSO only.", + "SsoUsersForm.ssoOnlyDescription": "New users can be created via SSO only.", + "SsoUsersForm.switcherLabel": "SSO users only", "StackTrace.jumpTo": "跳转至", "StackTrace.linkText": "在日志视图中打开", "StackTrace.loadLabel": "加载更多", diff --git a/app/src/common/utils/fieldTransformer.js b/app/src/common/utils/fieldTransformer.js new file mode 100644 index 0000000000..71544612c9 --- /dev/null +++ b/app/src/common/utils/fieldTransformer.js @@ -0,0 +1,35 @@ +export function combineNameAndEmailToFrom(inputObj) { + const obj = { ...inputObj }; + if (obj.fromName && obj.fromEmail) { + obj.from = `${obj.fromName} <${obj.fromEmail}>`; + delete obj.fromName; + delete obj.fromEmail; + } else { + obj.from = obj.fromName || obj.fromEmail; + delete obj.fromName; + delete obj.fromEmail; + } + return obj; +} + +export function separateFromIntoNameAndEmail(inputObj) { + const obj = { ...inputObj }; + if (obj.from) { + const match = obj.from.match(/^(.*) <(.*)>$/); + if (match) { + obj.fromName = match[1]; + obj.fromEmail = match[2]; + } else if (obj.from.includes('@')) { + obj.fromName = ''; + obj.fromEmail = obj.from; + } else { + obj.fromName = obj.from; + obj.fromEmail = ''; + } + delete obj.from; + } else { + obj.fromName = ''; + obj.fromEmail = ''; + } + return obj; +} diff --git a/app/src/common/utils/fieldTransformer.test.js b/app/src/common/utils/fieldTransformer.test.js new file mode 100644 index 0000000000..6bd9d9a7f5 --- /dev/null +++ b/app/src/common/utils/fieldTransformer.test.js @@ -0,0 +1,73 @@ +import { separateFromIntoNameAndEmail, combineNameAndEmailToFrom } from './fieldTransformer'; + +describe('separateFromIntoNameAndEmail', () => { + it('should split "from" into "fromName" and "fromEmail" when valid format is provided', () => { + const input = { from: 'John Doe ' }; + const result = separateFromIntoNameAndEmail(input); + expect(result).toEqual({ + fromName: 'John Doe', + fromEmail: 'john.doe@example.com', + }); + }); + + it('should set "fromName" and empty "fromEmail" when "from" does not include ', () => { + const input = { from: 'John Doe' }; + const result = separateFromIntoNameAndEmail(input); + expect(result).toEqual({ + fromName: 'John Doe', + fromEmail: '', + }); + }); + + it('should set "fromName" and "fromEmail" to empty strings when "from" is not provided', () => { + const input = {}; + const result = separateFromIntoNameAndEmail(input); + expect(result).toEqual({ + fromName: '', + fromEmail: '', + }); + }); + + it('should leave unrelated fields in the object unchanged', () => { + const input = { from: 'John Doe ', otherField: 'value' }; + const result = separateFromIntoNameAndEmail(input); + expect(result).toEqual({ + fromName: 'John Doe', + fromEmail: 'john.doe@example.com', + otherField: 'value', + }); + }); +}); + +describe('combineNameAndEmailToFrom', () => { + it('should combine "fromName" and "fromEmail" into "from"', () => { + const input = { fromName: 'John Doe', fromEmail: 'john.doe@example.com' }; + const result = combineNameAndEmailToFrom(input); + expect(result).toEqual({ + from: 'John Doe ', + }); + }); + + it('should leave unrelated fields in the object unchanged', () => { + const input = { fromName: 'John Doe', fromEmail: 'john.doe@example.com', otherField: 'value' }; + const result = combineNameAndEmailToFrom(input); + expect(result).toEqual({ + from: 'John Doe ', + otherField: 'value', + }); + }); + + it('should set "from" to "fromName" or "fromEmail" if only one is provided', () => { + const input1 = { fromName: 'John Doe' }; + const input2 = { fromEmail: 'john.doe@example.com' }; + const input3 = {}; + + const result1 = combineNameAndEmailToFrom(input1); + const result2 = combineNameAndEmailToFrom(input2); + const result3 = combineNameAndEmailToFrom(input3); + + expect(result1).toEqual({ from: 'John Doe' }); + expect(result2).toEqual({ from: 'john.doe@example.com' }); + expect(result3).toEqual({}); + }); +}); diff --git a/app/src/common/utils/index.js b/app/src/common/utils/index.js index 11999bb039..c905474508 100644 --- a/app/src/common/utils/index.js +++ b/app/src/common/utils/index.js @@ -58,3 +58,4 @@ export { omit } from './omit'; export { calculateFontColor } from './calculateFontColor'; export { createExternalLink } from './createExternalLink'; export { findAssignedProjectByOrganization } from './findAssignedProjectByOrganization'; +export { combineNameAndEmailToFrom, separateFromIntoNameAndEmail } from './fieldTransformer'; diff --git a/app/src/components/integrations/containers/integrationInfoContainer/instancesSection/instancesSection.jsx b/app/src/components/integrations/containers/integrationInfoContainer/instancesSection/instancesSection.jsx index 33ee681ea2..78bddb0012 100644 --- a/app/src/components/integrations/containers/integrationInfoContainer/instancesSection/instancesSection.jsx +++ b/app/src/components/integrations/containers/integrationInfoContainer/instancesSection/instancesSection.jsx @@ -42,7 +42,8 @@ import { isPluginBuiltin, } from 'components/integrations/utils'; import { PLUGIN_NAME_TITLES } from 'components/integrations/constants'; -import { LDAP } from 'common/constants/pluginNames'; +import { EMAIL, LDAP } from 'common/constants/pluginNames'; +import { combineNameAndEmailToFrom } from 'common/utils'; import { InstancesList } from './instancesList'; import styles from './instancesSection.scss'; @@ -188,10 +189,11 @@ export class InstancesSection extends Component { createIntegration = (formData, metaData) => { const { isGlobal, instanceType } = this.props; + const updatedFormData = instanceType === EMAIL ? combineNameAndEmailToFrom(formData) : formData; const data = { enabled: true, - integrationParameters: formData, - name: formData.integrationName || PLUGIN_NAME_TITLES[instanceType], + integrationParameters: updatedFormData, + name: updatedFormData.integrationName || PLUGIN_NAME_TITLES[instanceType], }; this.props.addIntegrationAction( diff --git a/app/src/components/integrations/containers/integrationSettingsContainer/integrationSettingsContainer.jsx b/app/src/components/integrations/containers/integrationSettingsContainer/integrationSettingsContainer.jsx index 813a5e2390..660c354458 100644 --- a/app/src/components/integrations/containers/integrationSettingsContainer/integrationSettingsContainer.jsx +++ b/app/src/components/integrations/containers/integrationSettingsContainer/integrationSettingsContainer.jsx @@ -22,6 +22,8 @@ import { updateIntegrationAction } from 'controllers/plugins'; import { uiExtensionIntegrationSettingsSelector } from 'controllers/plugins/uiExtensions/selectors'; import { INTEGRATIONS_SETTINGS_COMPONENTS_MAP } from 'components/integrations/settingsComponentsMap'; import { ExtensionLoader, extensionType } from 'components/extensionLoader'; +import { EMAIL } from 'common/constants/pluginNames'; +import { combineNameAndEmailToFrom } from 'common/utils'; import styles from './integrationSettingsContainer.scss'; const cx = classNames.bind(styles); @@ -61,13 +63,14 @@ export class IntegrationSettingsContainer extends Component { }, isGlobal, } = this.props; + const updatedFormData = pluginName === EMAIL ? combineNameAndEmailToFrom(formData) : formData; const data = { enabled: true, - integrationParameters: formData, + integrationParameters: updatedFormData, }; - if (formData.integrationName) { - data.name = formData.integrationName; + if (updatedFormData.integrationName) { + data.name = updatedFormData.integrationName; } this.props.updateIntegrationAction( diff --git a/app/src/components/integrations/integrationProviders/emailIntegration/constants.js b/app/src/components/integrations/integrationProviders/emailIntegration/constants.js index b3198eaa16..c233e51d7d 100644 --- a/app/src/components/integrations/integrationProviders/emailIntegration/constants.js +++ b/app/src/components/integrations/integrationProviders/emailIntegration/constants.js @@ -18,7 +18,8 @@ export const AUTH_ENABLED_KEY = 'authEnabled'; export const PROTOCOL_KEY = 'protocol'; export const SSL_KEY = 'sslEnabled'; export const TLS_KEY = 'starTlsEnabled'; -export const FROM_KEY = 'from'; +export const FROM_NAME_KEY = 'fromName'; +export const FROM_EMAIL_KEY = 'fromEmail'; export const HOST_KEY = 'host'; export const PORT_KEY = 'port'; export const USERNAME_KEY = 'username'; diff --git a/app/src/components/integrations/integrationProviders/emailIntegration/emailFormFields/emailFormFields.jsx b/app/src/components/integrations/integrationProviders/emailIntegration/emailFormFields/emailFormFields.jsx index efa1c22c95..8976d81e8c 100644 --- a/app/src/components/integrations/integrationProviders/emailIntegration/emailFormFields/emailFormFields.jsx +++ b/app/src/components/integrations/integrationProviders/emailIntegration/emailFormFields/emailFormFields.jsx @@ -33,17 +33,19 @@ import { INTEGRATION_FORM } from 'components/integrations/elements'; import { FieldElement } from 'pages/inside/projectSettingsPageContainer/content/elements'; import { FieldText } from 'componentLibrary/fieldText'; import { Dropdown } from 'componentLibrary/dropdown'; +import { separateFromIntoNameAndEmail } from 'common/utils'; import { DEFAULT_FORM_CONFIG, AUTH_ENABLED_KEY, PROTOCOL_KEY, SSL_KEY, TLS_KEY, - FROM_KEY, + FROM_NAME_KEY, HOST_KEY, PORT_KEY, USERNAME_KEY, PASSWORD_KEY, + FROM_EMAIL_KEY, } from '../constants'; import styles from './emailFormFields.scss'; @@ -58,9 +60,13 @@ const messages = defineMessages({ id: 'EmailFormFields.protocolLabel', defaultMessage: 'Protocol', }, - fromLabel: { - id: 'EmailFormFields.fromLabel', - defaultMessage: 'Default sender name', + fromNameLabel: { + id: 'EmailFormFields.fromNameLabel', + defaultMessage: 'From name', + }, + fromEmailLabel: { + id: 'EmailFormFields.fromEmailLabel', + defaultMessage: 'From Email', }, portLabel: { id: 'EmailFormFields.portLabel', @@ -72,7 +78,7 @@ const messages = defineMessages({ }, usernameLabel: { id: 'EmailFormFields.usernameLabel', - defaultMessage: 'Sender email', + defaultMessage: 'Username', }, passwordLabel: { id: 'EmailFormFields.passwordLabel', @@ -115,7 +121,9 @@ export class EmailFormFields extends Component { } componentDidMount() { - this.props.initialize(this.props.initialData); + const { initialData } = this.props; + const preparedData = separateFromIntoNameAndEmail(initialData); + this.props.initialize(preparedData); } onChangeAuthAvailability = (event, value) => { @@ -160,8 +168,8 @@ export class EmailFormFields extends Component { @@ -170,13 +178,11 @@ export class EmailFormFields extends Component { @@ -184,10 +190,12 @@ export class EmailFormFields extends Component { @@ -207,17 +215,31 @@ export class EmailFormFields extends Component { {authEnabled && ( - - - - - + <> + + + + + + + + + + + )}
diff --git a/app/src/components/main/grid/gridHeader/headerCell/headerCell.jsx b/app/src/components/main/grid/gridHeader/headerCell/headerCell.jsx index cc5afcd644..cf34d82f46 100644 --- a/app/src/components/main/grid/gridHeader/headerCell/headerCell.jsx +++ b/app/src/components/main/grid/gridHeader/headerCell/headerCell.jsx @@ -82,7 +82,10 @@ export const HeaderCell = track()( className={cx('header-cell', computedClassName)} style={customProps.rawHeaderCellStylesConfig} > -
+
{Parser(FilterIcon)}
diff --git a/app/src/components/widgets/singleLevelWidgets/charts/investigatedTrendChart/config/launchModeConfig.js b/app/src/components/widgets/singleLevelWidgets/charts/investigatedTrendChart/config/launchModeConfig.js index 054240087d..0525eb05ab 100644 --- a/app/src/components/widgets/singleLevelWidgets/charts/investigatedTrendChart/config/launchModeConfig.js +++ b/app/src/components/widgets/singleLevelWidgets/charts/investigatedTrendChart/config/launchModeConfig.js @@ -33,9 +33,6 @@ export const getLaunchModeConfig = ({ }) => { const colors = {}; const columns = []; - // EPMRPP-96393 (GitHub #2381): Changed sorting from -item.number to startTime-based sorting - // for consistency across all chart widgets. This ensures chronological ordering - // based on actual launch times rather than launch names/numbers. const sortedResult = content.sort((a, b) => { const startTimeA = new Date(a.startTime); const startTimeB = new Date(b.startTime); diff --git a/app/src/controllers/appInfo/constants.js b/app/src/controllers/appInfo/constants.js index cbd0668eda..8f77bdbc71 100644 --- a/app/src/controllers/appInfo/constants.js +++ b/app/src/controllers/appInfo/constants.js @@ -17,6 +17,7 @@ export const APP_INFO_NAMESPACE = 'appInfo'; export const ANALYTICS_INSTANCE_KEY = 'server.details.instance'; export const ANALYTICS_ALL_KEY = 'server.analytics.all'; +export const SSO_USERS_ONLY_KEY = 'server.users.sso'; export const OLD_HISTORY_KEY = 'history_old'; export const GA_MEASUREMENT_ID = 'ga_measurement_id'; export const INSTANCE_TYPE = 'instance_type'; diff --git a/app/src/controllers/appInfo/index.js b/app/src/controllers/appInfo/index.js index 13b135d71c..76722080b9 100644 --- a/app/src/controllers/appInfo/index.js +++ b/app/src/controllers/appInfo/index.js @@ -29,5 +29,6 @@ export { isDemoInstanceSelector, areUserSuggestionsAllowedSelector, baseEventParametersSelector, + ssoUsersOnlySelector, } from './selectors'; export { ANALYTICS_ALL_KEY } from './constants'; diff --git a/app/src/controllers/appInfo/selectors.js b/app/src/controllers/appInfo/selectors.js index 919745c2b5..0434b4ad6c 100644 --- a/app/src/controllers/appInfo/selectors.js +++ b/app/src/controllers/appInfo/selectors.js @@ -32,6 +32,7 @@ import { NOT_PROVIDED, ALLOW_DELETE_ACCOUNT, USER_SUGGESTIONS, + SSO_USERS_ONLY_KEY, } from './constants'; export const appInfoSelector = (state) => state.appInfo || {}; @@ -58,6 +59,8 @@ export const analyticsEnabledSelector = (state) => extensionsConfigSelector(state)[ANALYTICS_ALL_KEY] === 'true'; export const analyzerExtensionsSelector = (state) => extensionsSelector(state).analyzers || []; export const authExtensionsSelector = (state) => uatInfoSelector(state).authExtensions || {}; +export const ssoUsersOnlySelector = (state) => + extensionsConfigSelector(state)[SSO_USERS_ONLY_KEY] === 'true'; export const isOldHistorySelector = (state) => environmentSelector(state)[OLD_HISTORY_KEY] === 'true'; export const isDemoInstanceSelector = (state) => !!apiJobsSelector(state).flushingDataTrigger; diff --git a/app/src/controllers/log/constants.js b/app/src/controllers/log/constants.js index 7c27f8d9a1..42d15509e2 100644 --- a/app/src/controllers/log/constants.js +++ b/app/src/controllers/log/constants.js @@ -15,7 +15,8 @@ */ import * as logLevels from 'common/constants/logLevels'; -import { formatSortingString, SORTING_ASC } from 'controllers/sorting'; +import { SORTING_ASC } from 'controllers/sorting/constants'; +import { formatSortingString } from 'controllers/sorting/utils'; export const NAMESPACE = 'log'; export const LOG_ITEMS_NAMESPACE = `${NAMESPACE}/logItems`; diff --git a/app/src/controllers/log/storageUtils.js b/app/src/controllers/log/storageUtils.js index ea02e1f643..b97182ca7f 100644 --- a/app/src/controllers/log/storageUtils.js +++ b/app/src/controllers/log/storageUtils.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getStorageItem, updateStorageItem } from 'common/utils'; +import { getStorageItem, updateStorageItem } from 'common/utils/storageUtils'; import { MARKDOWN } from 'common/constants/logViewModes'; import { LOG_TIME_FORMAT_ABSOLUTE } from 'controllers/user/constants'; import { diff --git a/app/src/pages/inside/projectSettingsPageContainer/content/integrations/integrationsList/integrationInfo/integrationInfo.jsx b/app/src/pages/inside/projectSettingsPageContainer/content/integrations/integrationsList/integrationInfo/integrationInfo.jsx index 5ef9c860cb..e22c221e75 100644 --- a/app/src/pages/inside/projectSettingsPageContainer/content/integrations/integrationsList/integrationInfo/integrationInfo.jsx +++ b/app/src/pages/inside/projectSettingsPageContainer/content/integrations/integrationsList/integrationInfo/integrationInfo.jsx @@ -43,6 +43,8 @@ import { INTEGRATIONS_SETTINGS_COMPONENTS_MAP } from 'components/integrations/se import { EmptyStatePage } from 'pages/inside/projectSettingsPageContainer/content/emptyStatePage'; import { PROJECT_SETTINGS_INTEGRATION } from 'analyticsEvents/projectSettingsPageEvents'; import { INTEGRATIONS } from 'common/constants/settingsTabs'; +import { EMAIL } from 'common/constants/pluginNames'; +import { combineNameAndEmailToFrom } from 'common/utils'; import { IntegrationHeader } from './integrationHeader'; import { AvailableIntegrations } from './availableIntegrations'; import { messages } from './messages'; @@ -111,10 +113,11 @@ export const IntegrationInfo = (props) => { }; const addProjectIntegration = (formData, metaData) => { + const updatedFormData = pluginName === EMAIL ? combineNameAndEmailToFrom(formData) : formData; const newData = { enabled: true, - integrationParameters: formData, - name: formData.integrationName || PLUGIN_NAME_TITLES[pluginName], + integrationParameters: updatedFormData, + name: updatedFormData.integrationName || PLUGIN_NAME_TITLES[pluginName], }; trackEvent(PROJECT_SETTINGS_INTEGRATION.CLICK_CREATE_INTEGRATION_MODAL(pluginName)); dispatch(addIntegrationAction(newData, false, pluginName, openIntegration, metaData)); @@ -140,13 +143,14 @@ export const IntegrationInfo = (props) => { trackEvent(PROJECT_SETTINGS_INTEGRATION.CLICK_ADD_PROJECT_INTEGRATION(pluginName)); }; const onUpdate = (formData, onConfirm, metaData) => { + const updatedFormData = pluginName === EMAIL ? combineNameAndEmailToFrom(formData) : formData; const newData = { enabled: true, - integrationParameters: formData, + integrationParameters: updatedFormData, }; - if (formData.integrationName) { - newData.name = formData.integrationName; + if (updatedFormData.integrationName) { + newData.name = updatedFormData.integrationName; } dispatch( diff --git a/app/src/pages/inside/projectSettingsPageContainer/content/notifications/modals/addEditNotificationModal/addEditNotificationModal.jsx b/app/src/pages/inside/projectSettingsPageContainer/content/notifications/modals/addEditNotificationModal/addEditNotificationModal.jsx index 3e8401518d..7b5473d9f4 100644 --- a/app/src/pages/inside/projectSettingsPageContainer/content/notifications/modals/addEditNotificationModal/addEditNotificationModal.jsx +++ b/app/src/pages/inside/projectSettingsPageContainer/content/notifications/modals/addEditNotificationModal/addEditNotificationModal.jsx @@ -36,6 +36,8 @@ import { RadioGroup } from 'componentLibrary/radioGroup'; import { EMAIL } from 'common/constants/pluginNames'; import { FieldTextFlex } from 'componentLibrary/fieldTextFlex'; import { ruleField } from 'pages/inside/projectSettingsPageContainer/content/notifications/propTypes'; +import { fetchProjectAction } from 'controllers/project/actionCreators'; +import { projectIdSelector } from 'controllers/pages'; import { capitalizeWord } from '../util'; import { RecipientsContainer } from './recipientsContainer'; import { LaunchNamesContainer } from './launchNamesContainer'; @@ -213,12 +215,14 @@ const AddEditNotificationModal = ({ }) => { const { formatMessage } = useIntl(); const dispatch = useDispatch(); + const projectId = useSelector(projectIdSelector); const [isEditorShown, setShowEditor] = React.useState(data.notification.attributes.length > 0); const attributesValue = useSelector((state) => attributesValueSelector(state, ATTRIBUTES_FIELD_KEY)) ?? []; useEffect(() => { initialize(data.notification); + dispatch(fetchProjectAction(projectId, false)); }, []); const caseOptions = [ diff --git a/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.jsx b/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.jsx index 3f39fe854c..2f383c4f15 100644 --- a/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.jsx +++ b/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.jsx @@ -89,7 +89,7 @@ export const UniqueErrorsGridWrapped = ({ parentLaunch, data, loading, ...rest } activeSorting: true, sortable: true, customProps: { - gridHeaderCellStyles: cx('matched-header'), + titleClassName: cx('matched-header'), }, sortingEventInfo: UNIQUE_ERRORS_PAGE_EVENTS.CLICK_MATCHED_TESTS_HEADER_CELL, }); diff --git a/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.scss b/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.scss index 98fd576166..7d16af924b 100644 --- a/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.scss +++ b/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.scss @@ -20,6 +20,7 @@ .cluster-header { padding-left: 37px; + width: 100%; } .matched-header { diff --git a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/authConfigurationTab.jsx b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/authConfigurationTab.jsx index d19f22b93e..ae0d996bb0 100644 --- a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/authConfigurationTab.jsx +++ b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/authConfigurationTab.jsx @@ -16,6 +16,7 @@ import classNames from 'classnames/bind'; import { GithubAuthForm } from './forms'; +import { SsoUsersForm } from './forms/ssoUsersForm'; import styles from './authConfigurationTab.scss'; const cx = classNames.bind(styles); @@ -23,5 +24,6 @@ const cx = classNames.bind(styles); export const AuthConfigurationTab = () => (
+
); diff --git a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/githubAuthForm/githubAuthForm.scss b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/githubAuthForm/githubAuthForm.scss index b4cb423e6f..61cbacd31f 100644 --- a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/githubAuthForm/githubAuthForm.scss +++ b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/githubAuthForm/githubAuthForm.scss @@ -16,5 +16,5 @@ .github-auth-form { position: relative; - margin-bottom: 20px; + margin-bottom: 30px; } diff --git a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/index.js b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/index.js index 74c592c333..f8cf40deb8 100644 --- a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/index.js +++ b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/index.js @@ -15,3 +15,4 @@ */ export { GithubAuthForm } from './githubAuthForm'; +export { SsoUsersForm } from './ssoUsersForm'; diff --git a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/index.js b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/index.js new file mode 100644 index 0000000000..ad04edbd75 --- /dev/null +++ b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/index.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 EPAM Systems + * + * 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. + */ + +export { SsoUsersForm } from './ssoUsersForm'; diff --git a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/ssoUsersForm.jsx b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/ssoUsersForm.jsx new file mode 100644 index 0000000000..c3d88defb1 --- /dev/null +++ b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/ssoUsersForm.jsx @@ -0,0 +1,119 @@ +/* + * Copyright 2024 EPAM Systems + * + * 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, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, useIntl } from 'react-intl'; +import classNames from 'classnames/bind'; +import { connect } from 'react-redux'; +import { InputBigSwitcher } from 'components/inputs/inputBigSwitcher'; +import { SectionHeader } from 'components/main/sectionHeader'; +import { ADMIN_SERVER_SETTINGS_PAGE_EVENTS } from 'components/main/analytics/events'; +import { ssoUsersOnlySelector, fetchAppInfoAction } from 'controllers/appInfo'; +import formStyles from 'pages/instance/serverSettingsPage/common/formController/formController.scss'; +import styles from './ssoUsersForm.scss'; + +const formCx = classNames.bind(formStyles); +const cx = classNames.bind(styles); + +const messages = defineMessages({ + switcherLabel: { + id: 'SsoUsersForm.switcherLabel', + defaultMessage: 'SSO users only', + }, + formHeader: { + id: 'SsoUsersForm.formHeader', + defaultMessage: 'Instance Invitations', + }, + ssoOnlyDescription: { + id: 'SsoUsersForm.ssoOnlyDescription', + defaultMessage: 'New users can be created via SSO only.', + }, + manualInvitesDescription: { + id: 'SsoUsersForm.manualInvitesDescription', + defaultMessage: + 'Users can manually send invitations for other users. If enabled new users can be created via SSO only.', + }, +}); + +const SsoUsersFormComponent = ({ enabled: enabledFromStore, fetchAppInfo }) => { + const { formatMessage } = useIntl(); + const [enabled, setEnabled] = useState(enabledFromStore); + const inputId = 'ssoUsersToggle'; + + useEffect(() => { + fetchAppInfo(); + }, [fetchAppInfo]); + + useEffect(() => { + setEnabled(enabledFromStore); + }, [enabledFromStore]); + + const getDescription = () => + formatMessage(enabled ? messages.ssoOnlyDescription : messages.manualInvitesDescription); + + const handleToggle = (value) => { + setEnabled(value); + }; + + return ( +
+
+ +
+
+
+ +
+
+ +
+ {getDescription()} +
+
+
+
+
+
+ ); +}; + +SsoUsersFormComponent.propTypes = { + enabled: PropTypes.bool, + fetchAppInfo: PropTypes.func.isRequired, +}; + +SsoUsersFormComponent.defaultProps = { + enabled: false, +}; + +const mapStateToProps = (state) => ({ + enabled: ssoUsersOnlySelector(state), +}); + +const mapDispatchToProps = { + fetchAppInfo: fetchAppInfoAction, +}; + +export const SsoUsersForm = connect(mapStateToProps, mapDispatchToProps)(SsoUsersFormComponent); diff --git a/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/ssoUsersForm.scss b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/ssoUsersForm.scss new file mode 100644 index 0000000000..72a77cfd68 --- /dev/null +++ b/app/src/pages/instance/serverSettingsPage/serverSettingsTabs/authConfigurationTab/forms/ssoUsersForm/ssoUsersForm.scss @@ -0,0 +1,81 @@ +/* + * Copyright 2024 EPAM Systems + * + * 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. + */ + + +.form-group { + display: flex; + justify-content: flex-start; + align-items: center; + width: 100%; + margin-bottom: 25px; + + @media (max-width: $SCREEN_XS_MAX) { + flex-direction: column; + align-items: flex-start; + margin-bottom: 15px; + } +} + +.form-group-label { + min-width: 210px; + width: 210px; + padding-right: 4px; + text-align: right; + font-size: 13px; + line-height: 13px; + color: $COLOR--charcoal-grey; + box-sizing: border-box; + + @media (max-width: $SCREEN_SM_MAX) { + min-width: 150px; + width: 150px; + } + @media (max-width: $SCREEN_XS_MAX) { + width: 100%; + margin-bottom: 8px; + padding: 0; + text-align: left; + } +} + +.form-group-content { + flex: 1; + padding: 0 15px; + + @media (max-width: $SCREEN_XS_MAX) { + padding: 0; + width: 100%; + } +} + +.input-container { + display: flex; + align-items: center; + gap: 15px; + min-height: 36px; + + @media (max-width: $SCREEN_XS_MAX) { + flex-direction: column; + align-items: flex-start; + } +} + +.description { + width: 300px; + font-size: 12px; + line-height: 1.5; + color: $COLOR--gray-60; +} \ No newline at end of file