diff --git a/app/localization/translated/be.json b/app/localization/translated/be.json index bb5a0cf464..e49c296492 100644 --- a/app/localization/translated/be.json +++ b/app/localization/translated/be.json @@ -938,9 +938,9 @@ "IgnoreInAAModal.textMultiple": "Вы ўпэўненыя, што хочаце ігнараваць элементы ў Аўта-Аналізу?", "IgnoreInAAModal.title": "Iгнараваць элемент ў Аўта-Аналізу", "IgnoreInAAModal.titleMultiple": "Iгнараваць элементы ў Аўта-Аналізу", + "ImportLaunchModal.note": "Увага:", "ImportModal.importConfirmation": "Пацвердзіць адмену", "ImportModal.incorrectFileFormat": "Няправільны фармат файла", - "ImportModal.note": "Увага:", "IncludeInAAModal.includeButton": "Уключыць", "IncludeInAAModal.successMessage": "Элемент паспяхова дададзены ў Аўта-Аналіз", "IncludeInAAModal.successMessageMultiple": "Элементы паспяхова дададзены ў Аўта-Аналіз", @@ -1572,6 +1572,7 @@ "PhotoControls.uploadError": "Не ўдалося абнавіць фота", "PhotoControls.uploadPhoto": "Запампаваць Фотаздымак", "PhotoControls.wasDeleted": "Фота паспяхова выдалена", + "PluginDropDown.ReportType": "Тып дакладу:", "PluginItem.disablePluginMessage": "Вы ўпэўнены, што хочаце адключыць плагін {pluginName}? Калі вы адключыце плагін, інфармацыя пра яго будзе хавацца ў {pluginLocation}, і карыстальнікі не змогуць з ім ўзаемадзейнічаць", "PluginItem.disablePluginTitle": "Адключыць плагін", "PluginItem.disabledPluginMessage": "Плагін быў адключаны", @@ -1580,6 +1581,7 @@ "PluginItem.enabledPluginMessage": "Плагін быў уключаны", "PluginItem.titleVersion": "версія {version}", "Plugins.disabled.bts": "не будзе паказаны ў наладах праекта. Карыстальнікі не змогуць стварыць дэфект або прымацаваць праблему да існуючага дэфекту ў СОД", + "Plugins.disabled.import": "Зараз у асобнік не ўключаны/запампаваны плагін {name}. Дакументацыя", "Plugins.disabled.notification": "не будзе паказаны ў наладах праекта. Карыстальнікі не змогуць атрымліваць абвесткі аб завершаных запусках, а таксама не змогуць адпраўляць запрашэння", "Plugins.disabled.other": "не будзе паказаны ў наладах праекта", "PluginsFilter.all": "Усе", diff --git a/app/localization/translated/ru.json b/app/localization/translated/ru.json index e1d405dd76..7d9c6784c9 100644 --- a/app/localization/translated/ru.json +++ b/app/localization/translated/ru.json @@ -938,9 +938,9 @@ "IgnoreInAAModal.textMultiple": "Вы уверены, что хотите игнорировать элементы в Авто-Анализ?", "IgnoreInAAModal.title": "Игнорировать элемент при Авто-Анализе", "IgnoreInAAModal.titleMultiple": "Игнорировать элементы при Авто-Анализе", + "ImportLaunchModal.note": "Внимание:", "ImportModal.importConfirmation": "Подтвердить отмену", "ImportModal.incorrectFileFormat": "Неправильный формат файла", - "ImportModal.note": "Внимание:", "IncludeInAAModal.includeButton": "Включить", "IncludeInAAModal.successMessage": "Элемент успешно добавлен в Авто-Анализ", "IncludeInAAModal.successMessageMultiple": "Элементы успешно добавлены в Авто-Анализ", @@ -1572,6 +1572,7 @@ "PhotoControls.uploadError": "Не удалось обновить фото", "PhotoControls.uploadPhoto": "Загрузить Фото", "PhotoControls.wasDeleted": "Фото успешно удалено", + "PluginDropDown.ReportType": "Тип отчёта:", "PluginItem.disablePluginMessage": "Вы действительно хотите отключить плагин {pluginName}? Если вы отключите плагин, информация о нем будет скрыта в {pluginLocation}, и пользователи не смогут с ним взаимодействовать", "PluginItem.disablePluginTitle": "Отключить плагин", "PluginItem.disabledPluginMessage": "Плагин был отключен", @@ -1580,6 +1581,7 @@ "PluginItem.enabledPluginMessage": "Плагин был включен", "PluginItem.titleVersion": "версия {version}", "Plugins.disabled.bts": "не будет показан в настройках проекта. Пользователи не смогут создать дефект или прикрепить проблему к существующему дефекту в СОД", + "Plugins.disabled.import": "В данный момент нет включенного/загруженного {name} плагина на инстансе. Документация", "Plugins.disabled.notification": "не будет показан в настройках проекта. Пользователи не смогут получать оповещения о завершенных запусках, а также не смогут отправлять приглашения", "Plugins.disabled.other": "не будет показан в настройках проекта", "PluginsFilter.all": "Все", diff --git a/app/localization/translated/uk.json b/app/localization/translated/uk.json index 890c36822d..c86c3ff930 100644 --- a/app/localization/translated/uk.json +++ b/app/localization/translated/uk.json @@ -938,9 +938,9 @@ "IgnoreInAAModal.textMultiple": "Ви впевнені, що хочете ігнорувати елементи в Авто-Аналіз?", "IgnoreInAAModal.title": "Ігнорувати елемент Авто-при Аналізі", "IgnoreInAAModal.titleMultiple": "Ігнорувати елементи Авто-при Аналізі", + "ImportLaunchModal.note": "Увага:", "ImportModal.importConfirmation": "Підтвердити скасування", "ImportModal.incorrectFileFormat": "Неправильний формат файла", - "ImportModal.note": "Увага:", "IncludeInAAModal.includeButton": "Включити", "IncludeInAAModal.successMessage": "Елемент успішно додано в Авто-Аналіз", "IncludeInAAModal.successMessageMultiple": "Елементи успішно додано в Авто-Аналіз", @@ -1572,6 +1572,7 @@ "PhotoControls.uploadError": "Фото Не вдалося оновити", "PhotoControls.uploadPhoto": "Фото Завантажити", "PhotoControls.wasDeleted": "Фото успішно видалено", + "PluginDropDown.ReportType": "Тип звіту:", "PluginItem.disablePluginMessage": "Ви впевнені, що хочете вимкнути плагін {pluginName}? Якщо ви вимкнете плагін, інформація про нього буде прихована в {pluginLocation}, і користувачі не можуть взаємодіяти з ним", "PluginItem.disablePluginTitle": "Вимкнути плагін", "PluginItem.disabledPluginMessage": "Плагін був відключений", @@ -1580,6 +1581,7 @@ "PluginItem.enabledPluginMessage": "Плагін був включений", "PluginItem.titleVersion": "версія {version}", "Plugins.disabled.bts": "не буде показаний в настроюваннях проекту. Користувачі не зможуть створити дефект або проблему прикріпити до існуючого дефекту в СОД", + "Plugins.disabled.import": "Жоден плагін {name} наразі не ввімкнено/завантажено в екземпляр. Документація", "Plugins.disabled.notification": "не буде показаний в настроюваннях проекту. Користувачі не зможуть отримувати оповіщення про завершених запусках, а також не зможуть відсилати запрошення", "Plugins.disabled.other": "не буде показаний в настроюваннях проекту", "PluginsFilter.all": "Всі", diff --git a/app/localization/translated/zh.json b/app/localization/translated/zh.json index 794c779314..ad4f0d124d 100644 --- a/app/localization/translated/zh.json +++ b/app/localization/translated/zh.json @@ -938,9 +938,9 @@ "IgnoreInAAModal.textMultiple": "您确定要在自动分析中忽略这些测试项吗?", "IgnoreInAAModal.title": "在自动分析中忽略测试项", "IgnoreInAAModal.titleMultiple": "在自动分析中忽略测试项", + "ImportLaunchModal.note": "注:", "ImportModal.importConfirmation": "确认取消", "ImportModal.incorrectFileFormat": "文件格式不正确", - "ImportModal.note": "注:", "IncludeInAAModal.includeButton": "包含", "IncludeInAAModal.successMessage": "测试项已成功包含在自动分析中", "IncludeInAAModal.successMessageMultiple": "测试项已成功包含在自动分析中", @@ -1572,6 +1572,7 @@ "PhotoControls.uploadError": "照片未成功上传", "PhotoControls.uploadPhoto": "上传照片", "PhotoControls.wasDeleted": "照片已删除", + "PluginDropDown.ReportType": "Report type:", "PluginItem.disablePluginMessage": "您确定要禁用插件{pluginName}吗?如果您禁用该插件,有关它的信息将在{pluginLocation}上被隐藏,用户将无法与之交互", "PluginItem.disablePluginTitle": "禁用插件", "PluginItem.disabledPluginMessage": "插件已禁用", @@ -1580,6 +1581,7 @@ "PluginItem.enabledPluginMessage": "插件已启用", "PluginItem.titleVersion": "{version}", "Plugins.disabled.bts": "{name}将在项目设置中隐藏。用户将无法在BTS中发布或关联问题", + "Plugins.disabled.import": "No {name} plugin is currently enabled/uploaded on the instance. Documentation", "Plugins.disabled.notification": "{name}将在项目设置中隐藏。用户将无法收到通知并为新用户发送邀请", "Plugins.disabled.other": "{name}将在项目设置中隐藏", "PluginsFilter.all": "全部", diff --git a/app/src/common/constants/pluginsGroupTypes.js b/app/src/common/constants/pluginsGroupTypes.js index 918a758290..8648d3e777 100644 --- a/app/src/common/constants/pluginsGroupTypes.js +++ b/app/src/common/constants/pluginsGroupTypes.js @@ -19,4 +19,5 @@ export const NOTIFICATION_GROUP_TYPE = 'NOTIFICATION'; export const AUTHORIZATION_GROUP_TYPE = 'AUTH'; export const BTS_GROUP_TYPE = 'BTS'; export const ANALYZER_GROUP_TYPE = 'ANALYZER'; +export const IMPORT_GROUP_TYPE = 'IMPORT'; export const OTHER_GROUP_TYPE = 'OTHER'; diff --git a/app/src/components/integrations/messages.jsx b/app/src/components/integrations/messages.jsx index 33f963b61c..305281fdf1 100644 --- a/app/src/components/integrations/messages.jsx +++ b/app/src/components/integrations/messages.jsx @@ -20,6 +20,7 @@ import { ANALYZER_GROUP_TYPE, AUTHORIZATION_GROUP_TYPE, BTS_GROUP_TYPE, + IMPORT_GROUP_TYPE, NOTIFICATION_GROUP_TYPE, OTHER_GROUP_TYPE, } from 'common/constants/pluginsGroupTypes'; @@ -80,6 +81,11 @@ const messages = defineMessages({ defaultMessage: '{name} will be hidden on project settings. RP users can not get notifications and send invitations for new users', }, + pluginDisabledImport: { + id: 'Plugins.disabled.import', + defaultMessage: + 'No {name} plugin is currently enabled/uploaded on the instance. Documentation', + }, pluginDisabledOther: { id: 'Plugins.disabled.other', defaultMessage: '{name} will be hidden on project settings', @@ -89,7 +95,8 @@ const messages = defineMessages({ export const PLUGIN_DISABLED_MESSAGES_BY_GROUP_TYPE = { [BTS_GROUP_TYPE]: messages.pluginDisabledBts, [NOTIFICATION_GROUP_TYPE]: messages.pluginDisabledNotification, - [OTHER_GROUP_TYPE]: messages.pluginDisabledOther, + [IMPORT_GROUP_TYPE]: messages.pluginDisabledImport, [AUTHORIZATION_GROUP_TYPE]: messages.pluginDisabledOther, [ANALYZER_GROUP_TYPE]: messages.pluginDisabledOther, + [OTHER_GROUP_TYPE]: messages.pluginDisabledOther, }; diff --git a/app/src/controllers/plugins/index.js b/app/src/controllers/plugins/index.js index 8584216bec..c025ca343c 100644 --- a/app/src/controllers/plugins/index.js +++ b/app/src/controllers/plugins/index.js @@ -53,6 +53,7 @@ export { isEmailIntegrationAvailableSelector, isBtsPluginsExistSelector, enabledBtsPluginsSelector, + enabledImportPluginsSelector, globalIntegrationsSelector, pluginsLoadingSelector, } from './selectors'; diff --git a/app/src/controllers/plugins/selectors.js b/app/src/controllers/plugins/selectors.js index 9e34ccaca4..34db3c3178 100644 --- a/app/src/controllers/plugins/selectors.js +++ b/app/src/controllers/plugins/selectors.js @@ -15,7 +15,11 @@ */ import { createSelector } from 'reselect'; -import { BTS_GROUP_TYPE, NOTIFICATION_GROUP_TYPE } from 'common/constants/pluginsGroupTypes'; +import { + BTS_GROUP_TYPE, + IMPORT_GROUP_TYPE, + NOTIFICATION_GROUP_TYPE, +} from 'common/constants/pluginsGroupTypes'; import { EMAIL } from 'common/constants/pluginNames'; import { filterAvailablePlugins, @@ -72,6 +76,10 @@ export const enabledBtsPluginsSelector = createSelector(pluginsSelector, (plugin plugins.filter((item) => item.groupType === BTS_GROUP_TYPE && item.enabled), ); +export const enabledImportPluginsSelector = createSelector(pluginsSelector, (plugins) => + plugins.filter((plugin) => plugin.groupType === IMPORT_GROUP_TYPE && plugin.enabled), +); + export const createNamedIntegrationsSelector = (integrationName, integrationsSelector) => createSelector(integrationsSelector, (integrations) => filterIntegrationsByName(integrations, integrationName), diff --git a/app/src/pages/admin/pluginsPage/pluginsToolbar/actionPanel/actionPanel.jsx b/app/src/pages/admin/pluginsPage/pluginsToolbar/actionPanel/actionPanel.jsx index 932c28cb7c..6ea41346ee 100644 --- a/app/src/pages/admin/pluginsPage/pluginsToolbar/actionPanel/actionPanel.jsx +++ b/app/src/pages/admin/pluginsPage/pluginsToolbar/actionPanel/actionPanel.jsx @@ -26,7 +26,7 @@ import { GhostButton } from 'components/buttons/ghostButton'; import { PLUGINS_PAGE_EVENTS } from 'components/main/analytics/events'; import ImportIcon from 'common/img/import-inline.svg'; import { URLS } from 'common/urls'; -import { MODAL_TYPE_UPLOAD_PLUGIN } from 'pages/common/modals/importModal/constants'; +import DOMPurify from 'dompurify'; import styles from './actionPanel.scss'; export const UPLOAD = 'upload'; @@ -90,13 +90,15 @@ export class ActionPanel extends Component { tracking.trackEvent(PLUGINS_PAGE_EVENTS.CLICK_UPLOAD_BTN); this.props.showModalAction({ - id: 'importModal', + id: 'importPluginModal', data: { - type: MODAL_TYPE_UPLOAD_PLUGIN, onImport: this.props.fetchPluginsAction, title: formatMessage(messages.modalTitle), importButton: formatMessage(messages.uploadButton), - tip: formatMessage(messages.uploadTip), + tip: formatMessage(messages.uploadTip, { + b: (data) => DOMPurify.sanitize(`${data}`), + span: (data) => DOMPurify.sanitize(`${data}`), + }), incorrectFileSize: formatMessage(messages.incorrectFileSize), url: URLS.plugin(), singleImport: true, diff --git a/app/src/pages/common/modals/importModal/dropzoneField/dropzoneField.jsx b/app/src/pages/common/modals/importModal/dropzoneField/dropzoneField.jsx new file mode 100644 index 0000000000..b6aae53acc --- /dev/null +++ b/app/src/pages/common/modals/importModal/dropzoneField/dropzoneField.jsx @@ -0,0 +1,144 @@ +/* + * 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 from 'react'; +import { useIntl } from 'react-intl'; +import Parser from 'html-react-parser'; +import classNames from 'classnames/bind'; +import Dropzone from 'react-dropzone'; +import PropTypes from 'prop-types'; +import { ImportFileIcon } from 'pages/common/modals/importModal/importFileIcon'; +import { uniqueId } from 'common/utils'; +import DropZoneIcon from 'common/img/shape-inline.svg'; +import styles from './dropzoneField.scss'; + +const cx = classNames.bind(styles); + +export const DropzoneField = ({ + disabled, + incorrectFileSize, + tip, + singleImport, + incorrectFileFormatMessage, + files, + setFiles, + maxFileSize, + acceptFileMimeTypes, +}) => { + const { formatMessage } = useIntl(); + + const acceptFile = acceptFileMimeTypes.join(','); + + const onDropAcceptedFileHandler = (file) => ({ + file, + valid: true, + id: uniqueId(), + isLoading: false, + uploaded: false, + uploadingProgress: 0, + }); + + const formValidationMessage = (validationProperties) => { + const validationMessages = { + incorrectFileFormat: formatMessage(incorrectFileFormatMessage), + incorrectFileSize, + }; + const validationMessage = []; + + Object.keys(validationProperties).forEach((message) => { + if (validationProperties[message]) { + validationMessage.push(validationMessages[message]); + } + }); + + return validationMessage.join('. ').trim(); + }; + + const validateFile = (file) => ({ + incorrectFileFormat: !acceptFileMimeTypes.includes(file.type), + incorrectFileSize: file.size > maxFileSize, + }); + + const addFileRejectMessage = (file) => { + const validationProperties = validateFile(file); + + return formValidationMessage(validationProperties); + }; + + const onDropRejectedFileHandler = (file) => ({ + file, + valid: false, + id: uniqueId(), + rejectMessage: addFileRejectMessage(file), + }); + + const onDrop = (acceptedFiles, rejectedFiles) => { + const accepted = acceptedFiles.map(onDropAcceptedFileHandler); + const rejected = rejectedFiles.map(onDropRejectedFileHandler); + + setFiles([...files, ...accepted, ...rejected]); + }; + + const onDelete = (id) => { + setFiles(files.filter((item) => item.id !== id)); + }; + + return ( + + {files.length === 0 && ( +
+
{Parser(DropZoneIcon)}
+

{Parser(tip)}

+
+ )} + {files.length > 0 && ( +
+ {files.map((item) => ( + + ))} +
+ )} +
+ ); +}; +DropzoneField.propTypes = { + disabled: PropTypes.bool, + incorrectFileSize: PropTypes.string, + tip: PropTypes.string, + singleImport: PropTypes.bool, + incorrectFileFormatMessage: PropTypes.object, + files: PropTypes.array, + setFiles: PropTypes.func, + maxFileSize: PropTypes.number.isRequired, + acceptFileMimeTypes: PropTypes.arrayOf(PropTypes.string).isRequired, +}; +DropzoneField.defaultProps = { + disabled: false, + incorrectFileSize: '', + tip: '', + singleImport: true, + incorrectFileFormatMessage: {}, + files: [], + setFiles: () => {}, +}; diff --git a/app/src/pages/common/modals/importModal/importModal.scss b/app/src/pages/common/modals/importModal/dropzoneField/dropzoneField.scss similarity index 82% rename from app/src/pages/common/modals/importModal/importModal.scss rename to app/src/pages/common/modals/importModal/dropzoneField/dropzoneField.scss index 3a5c87f01d..277ddb3e7c 100644 --- a/app/src/pages/common/modals/importModal/importModal.scss +++ b/app/src/pages/common/modals/importModal/dropzoneField/dropzoneField.scss @@ -1,5 +1,5 @@ -/*! - * Copyright 2019 EPAM Systems +/* + * 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. @@ -60,16 +60,3 @@ flex-wrap: wrap; padding: 20px; } - -.note-label { - margin-top: 20px; - color: $COLOR--tealish; - font-size: 13px; - font-family: $FONT-SEMIBOLD, sans-serif; -} - -.note-message { - font-family: $FONT-REGULAR, sans-serif; - font-size: 12px; - line-height: 1.5; -} diff --git a/app/src/pages/common/modals/importModal/index.js b/app/src/pages/common/modals/importModal/dropzoneField/index.js similarity index 88% rename from app/src/pages/common/modals/importModal/index.js rename to app/src/pages/common/modals/importModal/dropzoneField/index.js index ca97cb67ed..83d9ded81f 100644 --- a/app/src/pages/common/modals/importModal/index.js +++ b/app/src/pages/common/modals/importModal/dropzoneField/index.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 EPAM Systems + * 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. @@ -14,4 +14,4 @@ * limitations under the License. */ -export { ImportModal } from './importModal'; +export DropzoneField from './dropzoneField'; diff --git a/app/src/pages/common/modals/importModal/importLaunchModal/importLaunchModal.jsx b/app/src/pages/common/modals/importModal/importLaunchModal/importLaunchModal.jsx new file mode 100644 index 0000000000..cdc519a122 --- /dev/null +++ b/app/src/pages/common/modals/importModal/importLaunchModal/importLaunchModal.jsx @@ -0,0 +1,72 @@ +/* + * 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, { useState } from 'react'; +import classNames from 'classnames/bind'; +import Parser from 'html-react-parser'; +import { defineMessages, useIntl } from 'react-intl'; +import { withModal } from 'controllers/modal'; +import PropTypes from 'prop-types'; +import { PluginDropDown } from 'pages/inside/launchesPage/pluginDropDown'; +import { ImportModalLayout } from '../importModalLayout/importModalLayout'; +import styles from './importLaunchModal.scss'; + +const cx = classNames.bind(styles); + +const messages = defineMessages({ + note: { + id: 'ImportLaunchModal.note', + defaultMessage: 'Note:', + }, +}); + +// todo get it from selectedPluginData.details +const MAX_FILE_SIZES = 33554432; +const ACCEPT_FILE_MIME_TYPES = [ + 'application/zip', + 'application/x-zip-compressed', + 'application/zip-compressed', + 'application/xml', + 'text/xml', +]; + +export const ImportLaunchModal = ({ data }) => { + const { formatMessage } = useIntl(); + const [selectedPluginData, setSelectedPluginData] = useState(); + + // todo use selectedPluginData for get supported format and max size + console.log(selectedPluginData); + + return ( + + +

{formatMessage(messages.note)}

+

{Parser(data.noteMessage)}

+
+ ); +}; +ImportLaunchModal.propTypes = { + data: PropTypes.object.isRequired, +}; +export default withModal('importLaunchModal')(ImportLaunchModal); diff --git a/app/src/pages/common/modals/importModal/importLaunchModal/importLaunchModal.scss b/app/src/pages/common/modals/importModal/importLaunchModal/importLaunchModal.scss new file mode 100644 index 0000000000..838c826893 --- /dev/null +++ b/app/src/pages/common/modals/importModal/importLaunchModal/importLaunchModal.scss @@ -0,0 +1,28 @@ +/*! + * 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. + */ + +.note-label { + margin-top: 20px; + color: $COLOR--tealish; + font-size: 13px; + font-family: $FONT-SEMIBOLD, sans-serif; +} + +.note-message { + font-family: $FONT-REGULAR, sans-serif; + font-size: 12px; + line-height: 1.5; +} diff --git a/app/src/pages/common/modals/importModal/constants.js b/app/src/pages/common/modals/importModal/importLaunchModal/index.js similarity index 52% rename from app/src/pages/common/modals/importModal/constants.js rename to app/src/pages/common/modals/importModal/importLaunchModal/index.js index 27d1332ea7..ca59e5decf 100644 --- a/app/src/pages/common/modals/importModal/constants.js +++ b/app/src/pages/common/modals/importModal/importLaunchModal/index.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 EPAM Systems + * 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. @@ -14,21 +14,4 @@ * limitations under the License. */ -export const MODAL_TYPE_IMPORT_LAUNCH = 'import'; -export const MODAL_TYPE_UPLOAD_PLUGIN = 'upload'; - -export const ACCEPT_FILE_MIME_TYPES = { - [MODAL_TYPE_IMPORT_LAUNCH]: [ - 'application/zip', - 'application/x-zip-compressed', - 'application/zip-compressed', - 'application/xml', - 'text/xml', - ], - [MODAL_TYPE_UPLOAD_PLUGIN]: ['.jar'], -}; - -export const MAX_FILE_SIZES = { - [MODAL_TYPE_IMPORT_LAUNCH]: 33554432, - [MODAL_TYPE_UPLOAD_PLUGIN]: 134217728, -}; +export { ImportLaunchModal } from './importLaunchModal'; diff --git a/app/src/pages/common/modals/importModal/importModal.jsx b/app/src/pages/common/modals/importModal/importModalLayout/importModalLayout.jsx similarity index 67% rename from app/src/pages/common/modals/importModal/importModal.jsx rename to app/src/pages/common/modals/importModal/importModalLayout/importModalLayout.jsx index a775a34f78..993c6ac035 100644 --- a/app/src/pages/common/modals/importModal/importModal.jsx +++ b/app/src/pages/common/modals/importModal/importModalLayout/importModalLayout.jsx @@ -16,28 +16,16 @@ import React, { Component, Fragment } from 'react'; import track from 'react-tracking'; -import classNames from 'classnames/bind'; -import Dropzone from 'react-dropzone'; -import Parser from 'html-react-parser'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { injectIntl, defineMessages } from 'react-intl'; import { ModalLayout, withModal } from 'components/main/modal'; import { showNotification, NOTIFICATION_TYPES } from 'controllers/notification'; import { COMMON_LOCALE_KEYS } from 'common/constants/localization'; -import { uniqueId, fetch } from 'common/utils'; -import DropZoneIcon from 'common/img/shape-inline.svg'; -import { ImportFileIcon } from './importFileIcon'; -import styles from './importModal.scss'; -import { ACCEPT_FILE_MIME_TYPES, MAX_FILE_SIZES } from './constants'; - -const cx = classNames.bind(styles); +import { fetch } from 'common/utils'; +import { DropzoneField } from 'pages/common/modals/importModal/dropzoneField/dropzoneField'; const messages = defineMessages({ - note: { - id: 'ImportModal.note', - defaultMessage: 'Note:', - }, importConfirmation: { id: 'ImportModal.importConfirmation', defaultMessage: 'Confirm cancel', @@ -54,7 +42,7 @@ const messages = defineMessages({ showNotification, }) @track() -export class ImportModal extends Component { +export class ImportModalLayout extends Component { static propTypes = { intl: PropTypes.object.isRequired, showNotification: PropTypes.func.isRequired, @@ -63,50 +51,22 @@ export class ImportModal extends Component { trackEvent: PropTypes.func, getTrackingData: PropTypes.func, }).isRequired, + dropzoneCountNumber: PropTypes.number, + children: PropTypes.node, + maxFileSize: PropTypes.number.isRequired, + acceptFileMimeTypes: PropTypes.arrayOf(PropTypes.string).isRequired, }; static defaultProps = { data: { eventsInfo: { uploadButton: () => {}, cancelBtn: {}, closeIcon: {} } }, + dropzoneCountNumber: 0, + children: [null], }; state = { files: [], }; - onDrop = (acceptedFiles, rejectedFiles) => { - const { files: stateFiles } = this.state; - const accepted = acceptedFiles.map(this.onDropAcceptedFileHandler); - const rejected = rejectedFiles.map(this.onDropRejectedFileHandler); - - this.setState({ - files: [...stateFiles, ...accepted, ...rejected], - }); - }; - - onDropAcceptedFileHandler = (file) => ({ - file, - valid: true, - id: uniqueId(), - isLoading: false, - uploaded: false, - uploadingProgress: 0, - }); - - onDropRejectedFileHandler = (file) => ({ - file, - valid: false, - id: uniqueId(), - rejectMessage: this.addFileRejectMessage(file), - }); - - onDelete = (id) => { - const { files } = this.state; - - this.setState({ - files: files.filter((item) => item.id !== id), - }); - }; - getOkButtonConfig = (isLoading, uploadFinished) => { const { intl, @@ -159,43 +119,8 @@ export class ImportModal extends Component { this.isUploadInProgress() || (this.props.data.singleImport && this.state.files.length > 0); - validateFile = (file) => { - const { type } = this.props.data; - - return { - incorrectFileFormat: !ACCEPT_FILE_MIME_TYPES[type].includes(file.type), - incorrectFileSize: file.size > MAX_FILE_SIZES[type], - }; - }; - getFilesNames = (files) => files.map(({ file: { name } }) => name).join('#'); - formValidationMessage = (validationProperties) => { - const { - intl, - data: { incorrectFileSize }, - } = this.props; - const validationMessages = { - incorrectFileFormat: intl.formatMessage(messages.incorrectFileFormat), - incorrectFileSize, - }; - const validationMessage = []; - - Object.keys(validationProperties).forEach((message) => { - if (validationProperties[message]) { - validationMessage.push(validationMessages[message]); - } - }); - - return validationMessage.join('. ').trim(); - }; - - addFileRejectMessage = (file) => { - const validationProperties = this.validateFile(file); - - return this.formValidationMessage(validationProperties); - }; - uploadFilesOnOkClick = () => { const data = this.prepareDataForServerUploading(); @@ -329,14 +254,16 @@ export class ImportModal extends Component { render() { const { intl, - data: { type, title, tip, noteMessage, eventsInfo, singleImport }, + data: { title, eventsInfo, tip, incorrectFileSize, singleImport }, + children, + dropzoneCountNumber, + maxFileSize, + acceptFileMimeTypes, } = this.props; - const { files } = this.state; const validFiles = this.getValidFiles(); const loading = this.isUploadInProgress(); const uploadFinished = this.isUploadFinished(); - const acceptFile = ACCEPT_FILE_MIME_TYPES[type].join(','); return ( - - {files.length === 0 && ( -
-
{Parser(DropZoneIcon)}
-

{Parser(tip)}

-
- )} - {files.length > 0 && ( -
- {files.map((item) => ( - - ))} -
- )} -
- {noteMessage && ( - -

{intl.formatMessage(messages.note)}

-

{Parser(noteMessage)}

-
- )} + {children && + children.map((child, index) => { + return index === dropzoneCountNumber ? ( + // eslint-disable-next-line react/no-array-index-key + + this.setState({ files: f })} + maxFileSize={maxFileSize} + acceptFileMimeTypes={acceptFileMimeTypes} + /> + {child && child} + + ) : ( + child && child + ); + })}
); } diff --git a/app/src/pages/common/modals/importModal/importModalLayout/index.js b/app/src/pages/common/modals/importModal/importModalLayout/index.js new file mode 100644 index 0000000000..37c5dffc4d --- /dev/null +++ b/app/src/pages/common/modals/importModal/importModalLayout/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 { ImportModalLayout } from './importModalLayout'; diff --git a/app/src/pages/common/modals/importModal/importPluginModal/importPluginModal.jsx b/app/src/pages/common/modals/importModal/importPluginModal/importPluginModal.jsx new file mode 100644 index 0000000000..1551d7f7fc --- /dev/null +++ b/app/src/pages/common/modals/importModal/importPluginModal/importPluginModal.jsx @@ -0,0 +1,35 @@ +/* + * 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 from 'react'; +import { withModal } from 'controllers/modal'; +import PropTypes from 'prop-types'; +import { ImportModalLayout } from '../importModalLayout/importModalLayout'; + +const MAX_FILE_SIZES = 134217728; +const ACCEPT_FILE_MIME_TYPES = ['.jar']; + +export const ImportPluginModal = ({ data }) => ( + +); +ImportPluginModal.propTypes = { + data: PropTypes.object.isRequired, +}; +export default withModal('importPluginModal')(ImportPluginModal); diff --git a/app/src/pages/common/modals/importModal/importPluginModal/index.js b/app/src/pages/common/modals/importModal/importPluginModal/index.js new file mode 100644 index 0000000000..16c7f0b4b7 --- /dev/null +++ b/app/src/pages/common/modals/importModal/importPluginModal/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 { ImportPluginModal } from './importPluginModal'; diff --git a/app/src/pages/common/modals/index.js b/app/src/pages/common/modals/index.js index c0136eff35..d372fd8eca 100644 --- a/app/src/pages/common/modals/index.js +++ b/app/src/pages/common/modals/index.js @@ -15,4 +15,5 @@ */ export { ConfirmationModal } from './confirmationModal'; -export { ImportModal } from './importModal'; +export { ImportLaunchModal } from './importModal/importLaunchModal'; +export { ImportPluginModal } from './importModal/importPluginModal'; diff --git a/app/src/pages/inside/launchesPage/LaunchToolbar/actionPanel/actionPanel.jsx b/app/src/pages/inside/launchesPage/LaunchToolbar/actionPanel/actionPanel.jsx index db43309455..4fa9b79966 100644 --- a/app/src/pages/inside/launchesPage/LaunchToolbar/actionPanel/actionPanel.jsx +++ b/app/src/pages/inside/launchesPage/LaunchToolbar/actionPanel/actionPanel.jsx @@ -28,9 +28,13 @@ import { GhostMenuButton } from 'components/buttons/ghostMenuButton'; import { Breadcrumbs, breadcrumbDescriptorShape } from 'components/main/breadcrumbs'; import { breadcrumbsSelector, restorePathAction } from 'controllers/testItem'; import { LAUNCHES_PAGE_EVENTS } from 'components/main/analytics/events'; +import { PLUGIN_DISABLED_MESSAGES_BY_GROUP_TYPE } from 'components/integrations/messages'; +import { withTooltip } from 'componentLibrary/tooltip'; import { COMMON_LOCALE_KEYS } from 'common/constants/localization'; +import { IMPORT_GROUP_TYPE } from 'common/constants/pluginsGroupTypes'; import AddWidgetIcon from 'common/img/add-widget-inline.svg'; import ImportIcon from 'common/img/import-inline.svg'; +import { createExternalLink, docsReferences } from 'common/utils'; import RefreshIcon from './img/refresh-inline.svg'; import styles from './actionPanel.scss'; @@ -46,6 +50,25 @@ const messages = defineMessages({ }, }); +const DisabledImportButton = () => ( + + + +); + +const DisabledImportButtonTooltip = ({ tooltip }) => {tooltip}; +DisabledImportButtonTooltip.propTypes = { + tooltip: PropTypes.string.isRequired, +}; + +// fixme may be use popup instead tooltip? and convert string to html +const DisabledImportButtonWithTooltip = withTooltip({ + ContentComponent: DisabledImportButtonTooltip, + side: 'bottom', + noArrow: false, + tooltipWrapperClassName: cx('tooltip-wrapper'), +})(DisabledImportButton); + @connect( (state) => ({ breadcrumbs: breadcrumbsSelector(state), @@ -87,6 +110,7 @@ export class ActionPanel extends Component { activeFilterId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onAddNewWidget: PropTypes.func, finishedLaunchesCount: PropTypes.number, + importPlugins: PropTypes.array.isRequired, }; static defaultProps = { @@ -233,8 +257,11 @@ export class ActionPanel extends Component { restorePath, onAddNewWidget, finishedLaunchesCount, + importPlugins, } = this.props; const actionDescriptors = this.createActionDescriptors(); + const isImportPluginEnabled = importPlugins.length > 0; + const importPluginDisabledMessage = PLUGIN_DISABLED_MESSAGES_BY_GROUP_TYPE[IMPORT_GROUP_TYPE]; return (
@@ -253,9 +280,18 @@ export class ActionPanel extends Component {
{this.isShowImportButton() && (
- - - + {isImportPluginEnabled ? ( + + + + ) : ( + createExternalLink(data, docsReferences.pluginsDocs), + })} + /> + )}
)} {this.isShowWidgetButton() && ( diff --git a/app/src/pages/inside/launchesPage/LaunchToolbar/launchToolbar.jsx b/app/src/pages/inside/launchesPage/LaunchToolbar/launchToolbar.jsx index 450b7398f5..e8190ad332 100644 --- a/app/src/pages/inside/launchesPage/LaunchToolbar/launchToolbar.jsx +++ b/app/src/pages/inside/launchesPage/LaunchToolbar/launchToolbar.jsx @@ -41,6 +41,7 @@ export const LaunchToolbar = ({ onAddNewWidget, activeFilterId, finishedLaunchesCount, + importPlugins, }) => (
{!!selectedLaunches.length && ( @@ -70,6 +71,7 @@ export const LaunchToolbar = ({ activeFilterId={activeFilterId} onAddNewWidget={onAddNewWidget} finishedLaunchesCount={finishedLaunchesCount} + importPlugins={importPlugins} />
); @@ -92,6 +94,7 @@ LaunchToolbar.propTypes = { activeFilterId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onAddNewWidget: PropTypes.func, finishedLaunchesCount: PropTypes.number, + importPlugins: PropTypes.array.isRequired, }; LaunchToolbar.defaultProps = { selectedLaunches: [], diff --git a/app/src/pages/inside/launchesPage/index.js b/app/src/pages/inside/launchesPage/index.js index a5a8dddeec..278653a8db 100644 --- a/app/src/pages/inside/launchesPage/index.js +++ b/app/src/pages/inside/launchesPage/index.js @@ -19,4 +19,5 @@ export { MoveToDebugModal, LaunchCompareModal, LaunchAnalysisModal } from './mod export { DeleteItemsModal } from 'pages/inside/common/modals/deleteItemsModal'; export { EditItemModal } from 'pages/inside/common/modals/editItemModal'; export { EditItemsModal } from 'pages/inside/common/modals/editItemsModal'; -export { ImportModal } from 'pages/common/modals/importModal'; +export { ImportLaunchModal } from 'pages/common/modals/importModal/importLaunchModal'; +export { ImportPluginModal } from 'pages/common/modals/importModal/importPluginModal'; diff --git a/app/src/pages/inside/launchesPage/launchesPage.jsx b/app/src/pages/inside/launchesPage/launchesPage.jsx index 65839dd88f..e1109eaada 100644 --- a/app/src/pages/inside/launchesPage/launchesPage.jsx +++ b/app/src/pages/inside/launchesPage/launchesPage.jsx @@ -33,7 +33,6 @@ import { LAUNCH_ITEM_TYPES } from 'common/constants/launchItemTypes'; import { ANALYZER_TYPES } from 'common/constants/analyzerTypes'; import { IN_PROGRESS } from 'common/constants/testStatuses'; import { PaginationToolbar } from 'components/main/paginationToolbar'; -import { MODAL_TYPE_IMPORT_LAUNCH } from 'pages/common/modals/importModal/constants'; import { activeProjectSelector, userIdSelector } from 'controllers/user'; import { isDemoInstanceSelector } from 'controllers/appInfo'; import { projectConfigSelector } from 'controllers/project'; @@ -72,6 +71,7 @@ import { LaunchSuiteGrid } from 'pages/inside/common/launchSuiteGrid'; import { LaunchFiltersContainer } from 'pages/inside/common/launchFiltersContainer'; import { LaunchFiltersToolbar } from 'pages/inside/common/launchFiltersToolbar'; import { RefineFiltersPanel } from 'pages/inside/common/refineFiltersPanel'; +import { enabledImportPluginsSelector } from 'controllers/plugins'; import { DebugFiltersContainer } from './debugFiltersContainer'; import { LaunchToolbar } from './LaunchToolbar'; import { NoItemsDemo } from './noItemsDemo'; @@ -174,6 +174,7 @@ const messages = defineMessages({ projectSetting: projectConfigSelector(state), highlightItemId: prevTestItemSelector(state), isDemoInstance: isDemoInstanceSelector(state), + importPlugins: enabledImportPluginsSelector(state), }), { showModalAction, @@ -243,6 +244,7 @@ export class LaunchesPage extends Component { updateLaunchesLocallyAction: PropTypes.func.isRequired, highlightItemId: PropTypes.number, isDemoInstance: PropTypes.bool, + importPlugins: PropTypes.arrayOf(PropTypes.object).isRequired, }; static defaultProps = { @@ -642,17 +644,20 @@ export class LaunchesPage extends Component { const { intl: { formatMessage }, activeProject, + importPlugins, } = this.props; this.props.tracking.trackEvent(LAUNCHES_PAGE_EVENTS.CLICK_IMPORT_BTN); this.props.showModalAction({ - id: 'importModal', + id: 'importLaunchModal', data: { - type: MODAL_TYPE_IMPORT_LAUNCH, onImport: this.props.fetchLaunchesAction, title: formatMessage(messages.modalTitle), importButton: formatMessage(messages.importButton), - tip: formatMessage(messages.importTip), + tip: formatMessage(messages.importTip, { + b: (data) => DOMPurify.sanitize(`${data}`), + span: (data) => DOMPurify.sanitize(`${data}`), + }), incorrectFileSize: formatMessage(messages.incorrectFileSize), noteMessage: formatMessage(messages.noteMessage), importConfirmationWarning: formatMessage(messages.importConfirmationWarning), @@ -662,6 +667,7 @@ export class LaunchesPage extends Component { cancelBtn: LAUNCHES_MODAL_EVENTS.CANCEL_BTN_IMPORT_MODAL, closeIcon: LAUNCHES_MODAL_EVENTS.CLOSE_ICON_IMPORT_MODAL, }, + importPlugins, }, }); }; @@ -762,6 +768,7 @@ export class LaunchesPage extends Component { loading, debugMode, isDemoInstance, + importPlugins, } = this.props; const rowHighlightingConfig = { @@ -814,6 +821,7 @@ export class LaunchesPage extends Component { activeFilterId={debugMode ? ALL : activeFilterId} onAddNewWidget={this.showWidgetWizard} finishedLaunchesCount={finishedLaunchesCount} + importPlugins={importPlugins} /> {debugMode && ( { + const { formatMessage } = useIntl(); + + const [selectedPlugin, setSelectedPlugin] = useState({ + label: importPlugins?.[0]?.name, + value: importPlugins?.[0]?.name, + }); + + const pluginNamesOptions = importPlugins.map((plugin) => ({ + label: plugin.name, + value: plugin.name, + })); + + const onChangePluginName = (pluginName) => { + if (pluginName !== selectedPlugin.name) { + setSelectedPlugin({ + label: pluginName, + value: pluginName, + }); + setSelectedPluginData(importPlugins.filter((p) => p.name !== pluginName)?.[0]); + } + }; + + return ( +
+ {formatMessage(messages.reportType)} + +
+ ); +}; +PluginDropDown.propTypes = { + setSelectedPluginData: PropTypes.func.isRequired, + importPlugins: PropTypes.array.isRequired, +}; diff --git a/app/src/pages/inside/launchesPage/pluginDropDown/pluginDropDown.scss b/app/src/pages/inside/launchesPage/pluginDropDown/pluginDropDown.scss new file mode 100644 index 0000000000..85fcf466a2 --- /dev/null +++ b/app/src/pages/inside/launchesPage/pluginDropDown/pluginDropDown.scss @@ -0,0 +1,26 @@ +/*! + * 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. + */ + +.plugin-dropdown { + margin-bottom: 24px; + + .field-name { + margin-right: 12px; + font-size: 13px; + font-family: $FONT-REGULAR; + color: $COLOR--charcoal-grey; + } +} diff --git a/app/src/pages/inside/projectSettingsPageContainer/content/notifications/LinkComponent/LinkComponent.scss b/app/src/pages/inside/projectSettingsPageContainer/content/notifications/LinkComponent/LinkComponent.scss index 04c60abd92..22383f9cab 100644 --- a/app/src/pages/inside/projectSettingsPageContainer/content/notifications/LinkComponent/LinkComponent.scss +++ b/app/src/pages/inside/projectSettingsPageContainer/content/notifications/LinkComponent/LinkComponent.scss @@ -26,11 +26,11 @@ height: 22px; } -.icon { - width: 16px; - height: 16px; - - svg * { - fill: $COLOR--e-300; - } -} \ No newline at end of file +.icon { + width: 16px; + height: 16px; + + svg * { + fill: $COLOR--e-300; + } +} diff --git a/app/src/pages/inside/projectSettingsPageContainer/content/notifications/ruleGroup/ruleGroup.scss b/app/src/pages/inside/projectSettingsPageContainer/content/notifications/ruleGroup/ruleGroup.scss index 4fb9a882ba..98bb2dfd8a 100644 --- a/app/src/pages/inside/projectSettingsPageContainer/content/notifications/ruleGroup/ruleGroup.scss +++ b/app/src/pages/inside/projectSettingsPageContainer/content/notifications/ruleGroup/ruleGroup.scss @@ -25,7 +25,7 @@ } } - .integrate-configurations{ + .integrate-configurations { display: flex; gap: 24px; align-items: center; @@ -43,7 +43,7 @@ margin-top: 6px; } -.rule-section-layout{ +.rule-section-layout { margin-top: 26px; } @@ -64,7 +64,7 @@ max-width: initial; } -.rule-group-list{ +.rule-group-list { max-width: initial; }