diff --git a/sass/component-repository/components/BundlePreview.scss b/sass/component-repository/components/BundlePreview.scss index 795f17ddc..a40d2bdd5 100644 --- a/sass/component-repository/components/BundlePreview.scss +++ b/sass/component-repository/components/BundlePreview.scss @@ -8,6 +8,12 @@ display: flex; } + &__divider { + margin-top: 0; + margin-bottom: 0; + border: 1px solid $color-extra-light-grey-border; + } + &__image { > img { width: 70px; @@ -143,6 +149,7 @@ &__error-wrapper { display: flex; flex-direction: column; + margin-bottom: 10px; } &__error-message { diff --git a/sass/component-repository/components/ComponentUninstallProcessState.scss b/sass/component-repository/components/ComponentUninstallProcessState.scss new file mode 100644 index 000000000..aec4037fb --- /dev/null +++ b/sass/component-repository/components/ComponentUninstallProcessState.scss @@ -0,0 +1,49 @@ +.ComponentUninstallProcessState { + &__in-progress-body { + display: flex; + flex-direction: column; + margin-top: 17px; + } + + &__in-progress-body-progress-bar-text { + margin-bottom: 13px; + font-family: "Open Sans"; + font-size: 12px; + font-weight: 700; + } + + &__in-progress-body-progress-bar { + position: relative; + width: 100%; + height: 5px; + margin-bottom: 11px; + border: 1px solid $color-ecr-component-installed-button-border; + background-color: $color-ultra-grey-border; + } + + &__current-progress { + position: absolute; + top: 0; + left: 0; + width: 0; // Use the CSS variable to set the width of the child div + height: 100%; + background-color: $color-mid-blue; + + &--finished { + background-color: $color-mid-green; + } + + &--partial-error { + background-color: $color-mid-orange; + } + } + + &__in-progress-body-progress-bar-number { + margin-right: 5px; + } + + &__in-progress-body-progress-footer-wrapper { + display: flex; + justify-content: space-between; + } +} \ No newline at end of file diff --git a/sass/index.scss b/sass/index.scss index bc10840c1..3719b1941 100644 --- a/sass/index.scss +++ b/sass/index.scss @@ -181,3 +181,4 @@ @import "component-repository/components/HubRegistrySwitcher"; @import "component-repository/components/BundlePreview"; @import "mfe-container/MfeMenuContainer"; +@import "component-repository/components/ComponentUninstallProcessState"; diff --git a/sass/patternfly.scss b/sass/patternfly.scss index 806e4eb04..9e11199cd 100644 --- a/sass/patternfly.scss +++ b/sass/patternfly.scss @@ -47,6 +47,8 @@ $color-published: #6CA100; $color-unpublished: #A6A6A6; $color-draft: #F0AB00; $color-mid-blue: #0088CE; +$color-mid-green: #3BB800; +$color-mid-orange: #FCA907; $color-mid-blue-semi: rgba(0,136,206,.6); $color-row-selected: #def3ff; $color-tour-primary: #00A0DF; diff --git a/src/api/component-repository/components.js b/src/api/component-repository/components.js index b12734788..ecf8df473 100644 --- a/src/api/component-repository/components.js +++ b/src/api/component-repository/components.js @@ -4,10 +4,13 @@ import { COMPONENT_INSTALLATION_CREATED, COMPONENT_INSTALLATION_COMPLETED, COMPONENT_UNINSTALLATION_CREATED, - COMPONENT_UNINSTALLATION_COMPLETED, + // COMPONENT_UNINSTALLATION_COMPLETED, + // COMPONENT_UNINSTALLATION_ERROR, + COMPONENT_UNINSTALLATION_IN_PROGRESS, COMPONENT_USAGE_LIST, COMPONENT_INSTALL_PLAN, GET_COMPONENT_INSTALL_PLAN, + GET_COMPONENT_CURRENT_JOB_STATUS, } from 'test/mocks/component-repository/components'; import { makeRequest, METHODS } from '@entando/apimanager'; @@ -63,7 +66,19 @@ export const getECRComponentUninstall = code => ( uri: `/components/${code}/uninstall`, domain: '/digital-exchange', method: METHODS.GET, - mockResponse: COMPONENT_UNINSTALLATION_COMPLETED, + // mockResponse: COMPONENT_UNINSTALLATION_COMPLETED, + mockResponse: COMPONENT_UNINSTALLATION_IN_PROGRESS, + // mockResponse: COMPONENT_UNINSTALLATION_ERROR, + useAuthentication: true, + }) +); + +export const getECRComponentCurrentJobStatus = code => ( + makeRequest({ + uri: `/components/${code}/lastJob`, + domain: '/digital-exchange', + method: METHODS.GET, + mockResponse: GET_COMPONENT_CURRENT_JOB_STATUS, useAuthentication: true, }) ); diff --git a/src/helpers/pollApi.js b/src/helpers/pollApi.js index ec12c9499..959c3ffec 100644 --- a/src/helpers/pollApi.js +++ b/src/helpers/pollApi.js @@ -9,7 +9,7 @@ const pollApi = ({ if (stepFunction) { stepFunction(data.payload || {}); } - if (stopPollingConditionFn(data)) { + if (stopPollingConditionFn && stopPollingConditionFn(data)) { resolve(data); } else if (Number(new Date()) < endTime) { setTimeout(checkCondition, interval, resolve, reject); @@ -24,6 +24,8 @@ const pollApi = ({ } else { reject(response); } + }).catch((error) => { + reject(error); }); }; return new Promise(checkCondition); diff --git a/src/locales/en.js b/src/locales/en.js index 5a5ffbd05..1b28feb5a 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -1581,6 +1581,7 @@ export default { 'app.filterTypesSelect.bundleGroup': 'Bundle Group', 'hub.bundle.installation': 'Bundle Installation', 'hub.bundle.uninstallation': 'Bundle Uninstallation', + 'hub.bundle.uninstalled': 'Bundle Uninstalled', 'app.deploy': 'Deploy', 'app.deployed': 'Deployed', 'app.undeploy': 'Undeploy', @@ -1613,5 +1614,7 @@ export default { 'componentRepository.hub.epcInstalledTip': 'To correctly update the EPC menu, please refresh the AppBuilder page on your browser', 'ecr.componentUninstallError': 'Message error - Is not possible to uninstall the {name}', 'ecr.componentPartiallyDeleted': 'Some elements of the bundle were deleted manually. To completely uninstall the bundle and clear every internal reference please proceed with uninstallation.', + 'componentRepository.components.elementsUninstalled': 'Elements Uninstalled', + 'componentRepository.components.someNotUninstalled': 'Some components were not uninstalled', }, }; diff --git a/src/locales/it.js b/src/locales/it.js index 7d0100e4f..3e0ddd6b6 100644 --- a/src/locales/it.js +++ b/src/locales/it.js @@ -1581,6 +1581,7 @@ export default { 'app.filterTypesSelect.bundleGroup': 'Gruppo pacchetto', 'hub.bundle.installation': 'Installazione del pacchetto', 'hub.bundle.uninstallation': 'Disinstallazione del pacchetto', + 'hub.bundle.uninstalled': 'Pacchetto Disinstallato', 'app.deploy': 'Distribuire', 'app.deployed': 'Schierato', 'app.undeploy': 'Annulla distribuzione', @@ -1613,5 +1614,7 @@ export default { 'componentRepository.hub.epcInstalledTip': 'Per aggiornare il menu EPC, aggiorna la pagina di AppBuilder nel tuo browser', 'ecr.componentUninstallError': 'Messaggio di errore: non è possibile disinstallare il {name}', 'ecr.componentPartiallyDeleted': 'Alcuni elementi del bundle sono stati cancellati manualmente. Per disinstallare completamente il bundle e ripulire il sistema da ogni referenza interna, procedere con la disinstallazione.', + 'componentRepository.components.elementsUninstalled': 'Elementi Disinstallati', + 'componentRepository.components.someNotUninstalled': 'Alcuni elementi non sono stati disinstallati', }, }; diff --git a/src/locales/pt.js b/src/locales/pt.js index 8b56821fa..5c9fd69bb 100644 --- a/src/locales/pt.js +++ b/src/locales/pt.js @@ -1033,5 +1033,8 @@ export default { 'componentRepository.hub.epcInstalledTip': 'Para atualizar corretamente o menu EPC, atualize a página do AppBuilder em seu navegador', 'ecr.componentUninstallError': 'Mensagem de erro - Não é possível desinstalar o {name}', 'ecr.componentPartiallyDeleted': 'Alguns elementos do pacote foram excluídos manualmente. Para desinstalar completamente o pacote e limpar todas as referências internas, prossiga com a desinstalação.', + 'componentRepository.components.elementsUninstalled': 'Elementos Desinstalados', + 'hub.bundle.uninstalled': 'Pacote Desinstalado', + 'componentRepository.components.someNotUninstalled': 'Alguns elementos não foram desinstalados', }, }; diff --git a/src/state/component-repository/components/actions.js b/src/state/component-repository/components/actions.js index bc447e437..3d988e01d 100644 --- a/src/state/component-repository/components/actions.js +++ b/src/state/component-repository/components/actions.js @@ -39,6 +39,7 @@ import { getECRComponentInstallPlan, putECRComponentInstallPlan, postECRComponentInstallPlan, + getECRComponentCurrentJobStatus, } from 'api/component-repository/components'; import { setPage } from 'state/pagination/actions'; import { toggleLoading, setLoading } from 'state/loading/actions'; @@ -50,6 +51,8 @@ import { ECR_COMPONENT_UNINSTALLATION_STATUS_COMPLETED, ECR_COMPONENT_UNINSTALLATION_STATUS_ERROR, ECR_COMPONENT_INSTALLATION_STATUS_ROLLBACK, + ECR_COMPONENT_UNINSTALLATION_STATUS_PARTIAL_COMPLETED, + ECR_COMPONENT_CURRENT_JOB_STATUS_INSTALLING, } from 'state/component-repository/components/const'; import { setVisibleModal } from 'state/modal/actions'; import { MODAL_ID } from 'ui/component-repository/components/InstallationPlanModal'; @@ -129,10 +132,11 @@ export const componentInstallOngoingProgress = code => ({ }, }); -export const componentUninstallOngoingProgress = code => ({ +export const componentUninstallOngoingProgress = (code, apiResponsePayload) => ({ type: COMPONENT_UNINSTALL_ONGOING_PROGRESS, payload: { code, + apiResponsePayload, }, }); @@ -399,62 +403,98 @@ export const getInstallPlan = component => dispatch => ( }) ); -export const pollECRComponentUninstallStatus = (componentCode, stepFunction) => dispatch => ( - new Promise((resolve) => { - dispatch(startComponentUninstall(componentCode)); - pollApi({ - apiFn: () => getECRComponentUninstall(componentCode), - stopPollingConditionFn: ({ - payload, - }) => payload && [ - ECR_COMPONENT_UNINSTALLATION_STATUS_COMPLETED, - ECR_COMPONENT_UNINSTALLATION_STATUS_ERROR, - ].includes(payload.status), - timeout: POLLING_TIMEOUT_IN_MS, - stepFunction: payload => stepFunction(payload.progress), - }) - .then(({ - payload, - }) => { - if (payload.status === ECR_COMPONENT_UNINSTALLATION_STATUS_COMPLETED) { - dispatch(finishComponentUninstall(componentCode)); - dispatch(fetchSelectedBundleStatusWithCode(componentCode)); - dispatch(fetchMfeConfigList()); - dispatch(addToast( - { id: 'componentRepository.hub.epcInstalledTip' }, - TOAST_SUCCESS, - )); - } else { - dispatch(componentUninstallFailed(componentCode)); - } - }) - .catch((res) => { - const { - errors, +export const pollECRComponentUninstallStatus = (componentCode, stepFunction, pollInBackground) => + dispatch => ( + new Promise((resolve) => { + if (!pollInBackground) { + dispatch(startComponentUninstall(componentCode)); + } + pollApi({ + apiFn: () => getECRComponentUninstall(componentCode), + stopPollingConditionFn: ({ payload, - } = res; - if (payload && payload.status === ECR_COMPONENT_INSTALLATION_STATUS_IN_PROGRESS) { - dispatch(addToast( - { id: 'componentRepository.components.notifyInProgress' }, - TOAST_WARNING, - )); - dispatch(componentUninstallOngoingProgress(componentCode)); - } else { - dispatch(addToast( - { id: 'componentRepository.components.notifyFailedUninstall' }, - TOAST_WARNING, - )); - dispatch(componentUninstallFailed(componentCode)); - } - if (errors && errors.length) { - dispatch(addErrors(errors.map(err => err.message))); - errors.forEach(err => dispatch(addToast(err.message, TOAST_ERROR))); - } - resolve(res); + }) => payload && [ + ECR_COMPONENT_UNINSTALLATION_STATUS_COMPLETED, + ECR_COMPONENT_UNINSTALLATION_STATUS_ERROR, + ECR_COMPONENT_UNINSTALLATION_STATUS_PARTIAL_COMPLETED, + ].includes(payload.status), + timeout: POLLING_TIMEOUT_IN_MS, + stepFunction: payload => stepFunction(payload.progress, payload), }) - .finally(() => resolve()); - }) -); + .then((response) => { + const { + payload, + } = response; + if (payload.status === ECR_COMPONENT_UNINSTALLATION_STATUS_COMPLETED) { + dispatch(finishComponentUninstall(componentCode)); + dispatch(fetchSelectedBundleStatusWithCode(componentCode)); + dispatch(fetchMfeConfigList()); + if (!pollInBackground) { + dispatch(addToast( + { id: 'componentRepository.hub.epcInstalledTip' }, + TOAST_SUCCESS, + )); + } + } else if (!pollInBackground) { + dispatch(componentUninstallFailed(componentCode)); + } + }) + .catch((res) => { + const { + errors, + payload, + } = res; + + if (payload && payload.status === ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS) { + dispatch(addToast( + { id: 'componentRepository.components.notifyInProgress' }, + TOAST_WARNING, + )); + dispatch(componentUninstallOngoingProgress(componentCode, payload)); + } else { + dispatch(addToast( + { id: 'componentRepository.components.notifyFailedUninstall' }, + TOAST_WARNING, + )); + dispatch(componentUninstallFailed(componentCode)); + } + if (errors && errors.length) { + dispatch(addErrors(errors.map(err => err.message))); + errors.forEach(err => dispatch(addToast(err.message, TOAST_ERROR))); + } + return resolve(res); + }) + .finally(() => resolve()); + }) + ); + +export const pollECRComponentCurrentUninstallJob = + (componentCode, stepFunction) => + dispatch => ( + new Promise((resolve, reject) => { + dispatch(startComponentUninstall(componentCode)); + pollApi({ + apiFn: () => getECRComponentCurrentJobStatus(componentCode), + timeout: POLLING_TIMEOUT_IN_MS, + stepFunction: payload => stepFunction(0.8, payload), + }) + .then((response) => { + const { + payload, + } = response; + if (payload.status === ECR_COMPONENT_CURRENT_JOB_STATUS_INSTALLING) { + reject(new Error('Component is installig')); + } + }) + .catch((res) => { + dispatch(pollECRComponentUninstallStatus(componentCode, stepFunction, true)); + resolve(res); + }) + .finally(() => { + resolve(); + }); + }) + ); export const uninstallECRComponent = (componentCode, logProgress) => dispatch => ( new Promise((resolve) => { @@ -500,14 +540,25 @@ export const fetchECRComponents = (paginationMetadata = { dispatch(setECRComponents(data.payload)); dispatch(setPage(data.metaData)); + const pollStepFunction = (progress, payload) => { + dispatch(setInstallUninstallProgress(progress)); + // only needed for uninstall progress + if (payload && payload.componentId) { + dispatch(componentUninstallOngoingProgress(payload.componentId, payload)); + } + }; + if (data.payload.length) { data.payload.forEach(({ lastJob }) => { if (lastJob && lastJob.status === ECR_COMPONENT_INSTALLATION_STATUS_IN_PROGRESS) { - dispatch(pollECRComponentInstallStatus(lastJob.componentId)); + dispatch(pollECRComponentInstallStatus(lastJob.componentId, pollStepFunction)); } else if ( lastJob && lastJob.status === ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS ) { - dispatch(pollECRComponentUninstallStatus(lastJob.componentId)); + dispatch(pollECRComponentCurrentUninstallJob( + lastJob.componentId, + pollStepFunction, + )); } }); } diff --git a/src/state/component-repository/components/const.js b/src/state/component-repository/components/const.js index 3af5aae7a..bb2261a0d 100644 --- a/src/state/component-repository/components/const.js +++ b/src/state/component-repository/components/const.js @@ -10,4 +10,7 @@ export const ECR_COMPONENT_INSTALLATION_STATUS_ROLLBACK = 'INSTALL_ROLLBACK'; export const ECR_COMPONENT_UNINSTALLATION_STATUS_CREATED = 'UNINSTALL_CREATED'; export const ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS = 'UNINSTALL_IN_PROGRESS'; export const ECR_COMPONENT_UNINSTALLATION_STATUS_COMPLETED = 'UNINSTALL_COMPLETED'; +export const ECR_COMPONENT_UNINSTALLATION_STATUS_PARTIAL_COMPLETED = 'UNINSTALL_PARTIAL_COMPLETED'; export const ECR_COMPONENT_UNINSTALLATION_STATUS_ERROR = 'UNINSTALL_ERROR'; +export const ECR_COMPONENT_CURRENT_JOB_STATUS_INSTALLING = 'installing'; +export const ECR_COMPONENT_CURRENT_JOB_STATUS_UNINSTALLING = 'uninstalling'; diff --git a/src/state/component-repository/components/reducer.js b/src/state/component-repository/components/reducer.js index 09dfd99b9..27a4cc532 100644 --- a/src/state/component-repository/components/reducer.js +++ b/src/state/component-repository/components/reducer.js @@ -61,17 +61,50 @@ const updateComponentInfo = (listState, componentIndex, newProps) => { return newListState; }; -const markComponentLastInstallStatus = (state, componentCode, lastInstallStatus) => { +const markComponentLastInstallStatus = ( + state, componentCode, lastInstallStatus, + apiResponsePayload, +) => { const componentIndex = findComponentInListById(state, componentCode); if (componentIndex === -1) { return state; } - return updateComponentInfo(state, componentIndex, { lastInstallStatus }); + if (apiResponsePayload) { + return updateComponentInfo( + state, componentIndex, + { lastInstallStatus, lastInstallApiResponse: apiResponsePayload }, + ); + } + return updateComponentInfo( + state, componentIndex, + { lastInstallStatus }, + ); }; -const markComponentLastStatusAsClear = (state, componentCode) => ( - markComponentLastInstallStatus(state, componentCode, '') -); +const markComponentLastUninstallStatus = ( + state, componentCode, lastUninstallStatus, + apiResponsePayload, +) => { + const componentIndex = findComponentInListById(state, componentCode); + if (componentIndex === -1) { + return state; + } + if (apiResponsePayload) { + return updateComponentInfo( + state, componentIndex, + { lastUninstallStatus, lastInstallApiResponse: apiResponsePayload }, + ); + } + return updateComponentInfo( + state, componentIndex, + { lastUninstallStatus }, + ); +}; + +const markComponentLastStatusAsClear = (state, componentCode) => { + markComponentLastInstallStatus(state, componentCode, ''); + return markComponentLastUninstallStatus(state, componentCode, ''); +}; const markComponentLastStatusAsError = (state, componentCode) => ( markComponentLastInstallStatus(state, componentCode, ECR_COMPONENT_INSTALLATION_STATUS_ERROR) @@ -85,11 +118,12 @@ const markComponentLastStatusAsInstallInProgress = (state, componentCode) => ( ) ); -const markComponentLastStatusAsUninstallInProgress = (state, componentCode) => ( - markComponentLastInstallStatus( +const markComponentLastStatusAsUninstallInProgress = (state, componentCode, apiResponsePayload) => ( + markComponentLastUninstallStatus( state, componentCode, ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS, + apiResponsePayload, ) ); @@ -140,7 +174,10 @@ const list = (state = [], action = {}) => { return markComponentLastStatusAsInstallInProgress(state, action.payload.code); } case COMPONENT_UNINSTALL_ONGOING_PROGRESS: { - return markComponentLastStatusAsUninstallInProgress(state, action.payload.code); + return markComponentLastStatusAsUninstallInProgress( + state, action.payload.code, + action.payload.apiResponsePayload, + ); } default: return state; } @@ -280,7 +317,7 @@ const uninstallation = (state = {}, action = {}) => { case START_COMPONENT_UNINSTALLATION: { return { ...state, - [action.payload.code]: ECR_COMPONENT_INSTALLATION_STATUS_IN_PROGRESS, + [action.payload.code]: ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS, }; } case FINISH_COMPONENT_UNINSTALLATION: diff --git a/src/state/component-repository/components/selectors.js b/src/state/component-repository/components/selectors.js index 95676f6d6..c16cd2ca1 100644 --- a/src/state/component-repository/components/selectors.js +++ b/src/state/component-repository/components/selectors.js @@ -59,6 +59,11 @@ export const getECRComponentLastInstallStatus = (state, props) => { return get(state, `componentRepositoryComponents.list[${listIdx}].lastInstallStatus`, ''); }; +export const getECRComponentLastInstallApiResponsePayload = (state, props) => { + const listIdx = findComponentInListById(get(state, 'componentRepositoryComponents.list', []), props.component.code); + return get(state, `componentRepositoryComponents.list[${listIdx}].lastInstallApiResponse`, {}); +}; + export const getECRComponentInstallationStatus = createSelector( [state => state.componentRepositoryComponents.installation, (state, props) => props.component], (installation, component) => installation[component.code], @@ -69,6 +74,11 @@ export const getECRComponentUninstallStatus = createSelector( (uninstallation, component) => uninstallation[component.code], ); +export const getECRComponentsUninstallationStatuses = createSelector( + [state => state.componentRepositoryComponents.uninstallation], + uninstallation => uninstallation, +); + export const getComponentUsageList = state => ( get(state, 'componentRepositoryComponents.usageList', []) ); diff --git a/src/ui/common/modal/GenericModal.js b/src/ui/common/modal/GenericModal.js index 7ef5b2b4d..e36f1f131 100644 --- a/src/ui/common/modal/GenericModal.js +++ b/src/ui/common/modal/GenericModal.js @@ -13,11 +13,12 @@ const GenericModal = ({ buttons, modalFooter, modalTitle, + cancelTextKey, }) => { const footer = modalFooter || ( {buttons.map(button => ( , - ]; + ] : []; const modalTitle = ( @@ -41,13 +68,16 @@ const ComponentUninstallManagerModal = () => { } return ( c.exist === false) } @@ -58,8 +88,10 @@ const ComponentUninstallManagerModal = () => { { renderContent() diff --git a/src/ui/component-repository/components/item/install-controls/ComponentUninstallProcessState.js b/src/ui/component-repository/components/item/install-controls/ComponentUninstallProcessState.js new file mode 100644 index 000000000..333a21dc2 --- /dev/null +++ b/src/ui/component-repository/components/item/install-controls/ComponentUninstallProcessState.js @@ -0,0 +1,94 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import ComponentDependenciesTable from './ComponentDependenciesTable'; + +const UninstallInProgressOrFinished = (props) => { + const { lastInstallApiResponse } = props; + const errorComponents = lastInstallApiResponse && lastInstallApiResponse.errorComponents; + const showErrorTable = errorComponents && errorComponents.length > 0; + return ( +
+
+
+

+ +

+
+
+
+
+ + {`${Math.floor(props.progress * 100)}%`} + +
+
+ { + showErrorTable ? ( +
+
+

+ +

+
+
+ +
+ ) : null + } +
+ ); +}; + +UninstallInProgressOrFinished.propTypes = { + name: PropTypes.string.isRequired, + progress: PropTypes.number, + lastInstallApiResponse: PropTypes.shape({ + errorComponents: PropTypes.arrayOf(PropTypes.shape({ + code: PropTypes.string, + type: PropTypes.string, + status: PropTypes.string, + })), + }).isRequired, +}; + +UninstallInProgressOrFinished.defaultProps = { + progress: 0, +}; + +const ComponentUninstallProcessState = (props) => { + const { + progress, lastInstallApiResponse, name, + } = props; + return (); +}; + +ComponentUninstallProcessState.propTypes = { + name: PropTypes.string.isRequired, + progress: PropTypes.number.isRequired, + lastInstallApiResponse: PropTypes.shape({}).isRequired, +}; + +export default ComponentUninstallProcessState; diff --git a/src/ui/component-repository/components/item/install-controls/ComponentUninstallStart.js b/src/ui/component-repository/components/item/install-controls/ComponentUninstallStart.js index 954615f16..2c336566f 100644 --- a/src/ui/component-repository/components/item/install-controls/ComponentUninstallStart.js +++ b/src/ui/component-repository/components/item/install-controls/ComponentUninstallStart.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import ComponentDependenciesTable from 'ui/component-repository/components/item/install-controls/ComponentDependenciesTable'; +import ComponentUninstallProcessState from './ComponentUninstallProcessState'; const ComponentUninstallStart = (props) => { const { @@ -10,9 +11,13 @@ const ComponentUninstallStart = (props) => { }, componentDependencies, dependenciesPartiallyDeleted, + componentUninstallStatus, + progress, + lastInstallApiResponse, } = props; - const autoUninstallNotPossible = componentDependencies && componentDependencies.length > 0; + const autoUninstallNotPossible = componentDependencies && componentDependencies.length > 0 && + componentDependencies.filter(c => c.hasExternal).length > 0; return (
@@ -40,46 +45,59 @@ const ComponentUninstallStart = (props) => {
-
-
- -
-
- {description} -
-
{ - dependenciesPartiallyDeleted ? ( -
-

- +

+
+ +
+
+ {description} +
+
+ { + dependenciesPartiallyDeleted ? ( +
+

+ +

+
+
+ ) : null + } + { + autoUninstallNotPossible ? ( +
+

+ +

+
+
+ ) : null + } + { + autoUninstallNotPossible ? ( + c.hasExternal)} /> -

-
+ ) : null + }
- ) : null - } - { - autoUninstallNotPossible ? ( -
-

- -

-
-
- ) : null - } - { - autoUninstallNotPossible ? ( - c.hasExternal)} + ) : ( + - ) : null + ) }
); @@ -97,11 +115,17 @@ ComponentUninstallStart.propTypes = { code: PropTypes.string.isRequired, })), dependenciesPartiallyDeleted: PropTypes.bool, + componentUninstallStatus: PropTypes.string, + progress: PropTypes.number, + lastInstallApiResponse: PropTypes.shape({}), }; ComponentUninstallStart.defaultProps = { componentDependencies: [], dependenciesPartiallyDeleted: false, + componentUninstallStatus: '', + progress: 0, + lastInstallApiResponse: {}, }; export default ComponentUninstallStart; diff --git a/src/ui/component-repository/components/list/ComponentList.js b/src/ui/component-repository/components/list/ComponentList.js index 429424817..75d68ba1b 100644 --- a/src/ui/component-repository/components/list/ComponentList.js +++ b/src/ui/component-repository/components/list/ComponentList.js @@ -44,6 +44,7 @@ class ComponentList extends Component { openComponentManagementModal, bundleStatuses, openedModal, + componentUninstallationStatuses, } = this.props; const pagination = { @@ -56,14 +57,16 @@ class ComponentList extends Component { components={componentRepositoryComponents} locale={intl.locale} onClickInstallPlan={getInstallPlan} - openComponentManagementModal={openComponentManagementModal} + openComponentManagementModal={args => + openComponentManagementModal(args, componentUninstallationStatuses)} bundleStatuses={bundleStatuses} />) : ( + openComponentManagementModal(args, componentUninstallationStatuses)} bundleStatuses={bundleStatuses} />); @@ -118,6 +121,9 @@ ComponentList.propTypes = { installedVersion: PropTypes.string, })), openedModal: PropTypes.string, + componentUninstallationStatuses: PropTypes.shape({ + code: PropTypes.string, + }), }; ComponentList.defaultProps = { @@ -128,6 +134,7 @@ ComponentList.defaultProps = { openComponentManagementModal: () => {}, bundleStatuses: [], openedModal: '', + componentUninstallationStatuses: {}, }; export default injectIntl(ComponentList); diff --git a/src/ui/component-repository/components/list/ComponentListContainer.js b/src/ui/component-repository/components/list/ComponentListContainer.js index 6681e08e5..f2c231b2b 100644 --- a/src/ui/component-repository/components/list/ComponentListContainer.js +++ b/src/ui/component-repository/components/list/ComponentListContainer.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; -import { fetchECRComponents, getInstallPlan } from 'state/component-repository/components/actions'; -import { getECRComponentList, getECRComponentListViewMode } from 'state/component-repository/components/selectors'; +import { fetchECRComponents, getInstallPlan, setSelectedECRComponent } from 'state/component-repository/components/actions'; +import { getECRComponentList, getECRComponentListViewMode, getECRComponentsUninstallationStatuses } from 'state/component-repository/components/selectors'; import { getLoading } from 'state/loading/selectors'; import { getCurrentPage, getTotalItems, getPageSize } from 'state/pagination/selectors'; import ComponentList from 'ui/component-repository/components/list/ComponentList'; @@ -10,6 +10,7 @@ import { setInfo, setVisibleModal } from 'state/modal/actions'; import { HUB_BUNDLE_MANAGEMENT_MODAL_ID } from 'ui/component-repository/components/list/HubBundleManagementModal'; import { getBundleStatuses } from 'state/component-repository/hub/selectors'; import { getVisibleModal } from 'state/modal/selectors'; +import { ECR_COMPONENT_UNINSTALLATION_STATUS_CREATED, ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS } from 'state/component-repository/components/const'; const ecrLoading = 'component-repository/components'; @@ -22,6 +23,7 @@ export const mapStateToProps = state => ({ pageSize: getPageSize(state), bundleStatuses: getBundleStatuses(state), openedModal: getVisibleModal(state), + componentUninstallationStatuses: getECRComponentsUninstallationStatuses(state), }); export const mapDispatchToProps = dispatch => ({ @@ -34,7 +36,14 @@ export const mapDispatchToProps = dispatch => ({ getInstallPlan: (code) => { dispatch(getInstallPlan(code)); }, - openComponentManagementModal: (component) => { + openComponentManagementModal: (component, componentUninstallationStatuses) => { + const currentStatus = componentUninstallationStatuses[component.code]; + if (currentStatus === ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS || + currentStatus === ECR_COMPONENT_UNINSTALLATION_STATUS_CREATED) { + dispatch(setSelectedECRComponent(component)); + dispatch(setVisibleModal(`uninstall-manager-for-${component.code}`)); + return; + } dispatch(setInfo({ type: 'Component', payload: component })); dispatch(setVisibleModal(HUB_BUNDLE_MANAGEMENT_MODAL_ID)); }, diff --git a/src/ui/component-repository/components/list/HubBundleList.js b/src/ui/component-repository/components/list/HubBundleList.js index ac8c1d8a4..e719d2b7d 100644 --- a/src/ui/component-repository/components/list/HubBundleList.js +++ b/src/ui/component-repository/components/list/HubBundleList.js @@ -4,10 +4,10 @@ import { FormattedMessage, intlShape, injectIntl } from 'react-intl'; import { useSelector, useDispatch } from 'react-redux'; import HubBundleManagementModal, { HUB_BUNDLE_MANAGEMENT_MODAL_ID } from 'ui/component-repository/components/list/HubBundleManagementModal'; -import { ECR_COMPONENTS_GRID_VIEW } from 'state/component-repository/components/const'; +import { ECR_COMPONENTS_GRID_VIEW, ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS } from 'state/component-repository/components/const'; import paginatorMessages from 'ui/paginatorMessages'; import { getCurrentPage, getPageSize, getTotalItems } from 'state/pagination/selectors'; -import { getECRComponentListViewMode } from 'state/component-repository/components/selectors'; +import { getECRComponentListViewMode, getECRComponentsUninstallationStatuses } from 'state/component-repository/components/selectors'; import { fetchBundlesFromRegistry, fetchBundlesFromRegistryWithFilters, FETCH_BUNDLES_LOADING_STATE } from 'state/component-repository/hub/actions'; import { getLoading } from 'state/loading/selectors'; import { getBundlesFromRegistry, getBundleStatuses, getSelectedRegistry, getBundleGroups } from 'state/component-repository/hub/selectors'; @@ -36,6 +36,7 @@ const HubBundleList = ({ const bundleStatuses = useSelector(getBundleStatuses); const openedModal = useSelector(getVisibleModal); const bundleGroups = useSelector(getBundleGroups); + const componentUninstallationStatuses = useSelector(getECRComponentsUninstallationStatuses); useEffect( () => { dispatch(fetchBundlesFromRegistry(activeRegistry.id)); }, @@ -57,9 +58,14 @@ const HubBundleList = ({ }, [activeRegistry.id, dispatch]); const openComponentManagementModal = useCallback((component) => { + if (componentUninstallationStatuses[component.code] === + ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS) { + dispatch(setVisibleModal(`uninstall-manager-for-${component.code}`)); + return; + } dispatch(setInfo({ type: 'Component', payload: component })); dispatch(setVisibleModal(HUB_BUNDLE_MANAGEMENT_MODAL_ID)); - }, [dispatch]); + }, [componentUninstallationStatuses, dispatch]); const renderComponents = (viewMode === ECR_COMPONENTS_GRID_VIEW) ? ( { ); const component = { ...(selectedECRComponent || {}), ...(ecrComponent || {}) }; - + const lastInstallApiResponse = useSelector(state => + getECRComponentLastInstallApiResponsePayload(state, { component })) || {}; const isComponentInstalling = useSelector(state => getECRComponentInstallationStatus(state, { component: @@ -74,7 +77,7 @@ const HubBundleManagementModal = () => { useSelector(state => getECRComponentUninstallStatus(state, { component: { code: component.code }, - })) === ECR_COMPONENT_INSTALLATION_STATUS_IN_PROGRESS; + })) === ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS; const belongingBundleGroup = useMemo(() => { const belongingBundleGroups = bundlegroups @@ -152,6 +155,16 @@ const HubBundleManagementModal = () => { }, [dispatch, payload.gitRepoAddress, payload.repoUrl, selectedBundleStatus.status, redeployed]); + useEffect(() => { + if (lastInstallApiResponse && + (lastInstallApiResponse.status === ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS + || + lastInstallApiResponse.status === ECR_COMPONENT_CURRENT_JOB_STATUS_UNINSTALLING) + ) { + dispatch(setVisibleModal(`uninstall-manager-for-${component.code}`)); + } + }, [component.code, dispatch, lastInstallApiResponse]); + const refreshVersionsButton = ( { describe('filter reducer', () => { @@ -375,9 +375,9 @@ describe('uninstallation reducer', () => { expect(state).toHaveProperty('uninstallation'); expect(Object.keys(state.uninstallation)).toHaveLength(2); expect(state).toHaveProperty('uninstallation.test3'); - expect(state).toHaveProperty('uninstallation.test3', ECR_COMPONENT_INSTALLATION_STATUS_IN_PROGRESS); + expect(state).toHaveProperty('uninstallation.test3', ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS); expect(state).toHaveProperty('uninstallation.test4'); - expect(state).toHaveProperty('uninstallation.test4', ECR_COMPONENT_INSTALLATION_STATUS_IN_PROGRESS); + expect(state).toHaveProperty('uninstallation.test4', ECR_COMPONENT_UNINSTALLATION_STATUS_IN_PROGRESS); }); }); diff --git a/test/test/mocks/component-repository/components.js b/test/test/mocks/component-repository/components.js index 383941a09..f10008a41 100644 --- a/test/test/mocks/component-repository/components.js +++ b/test/test/mocks/component-repository/components.js @@ -216,6 +216,30 @@ const componentInstallation = { user: 'admin', progress: 1, errorMessage: null, + id: '0010efcf-76fa-46d9-aa17-de59a355ef7a', + startedAt: '2023-07-21T07:27:32.568937', + finishedAt: '2023-07-21T07:27:37.830153', + userId: '', + status: 'UNINSTALL_PARTIAL_COMPLETED', // UNINSTALL_CREATED, UNINSTALL_IN_PROGRESS, UNINSTALL_PARTIAL_COMPLETED, UNINSTALL_ERROR, UNINSTALL_COMPLETED + uninstallErrorCode: '100', + uninstallErrorMessage: 'Error in deleting component from app-engine', // sample message, it might change based on the error + errorComponents: [ + { + type: 'content', + code: 'NWS12', + status: 'failure', + }, + { + type: 'page', + code: 'page1', + status: 'failure', + }, + { + type: 'category', + code: 'cat1', + status: 'failure', + }, + ], }; export const COMPONENT_INSTALLATION_CREATED = { @@ -465,3 +489,9 @@ export const GET_COMPONENT_INSTALL_PLAN = { customInstallation: false, }; +export const GET_COMPONENT_CURRENT_JOB_STATUS = { + payload: { + status: 'uninstalling', // 'installing' + }, + errors: [], +}; diff --git a/test/ui/component-repository/components/item/install-controls/ComponentInstallActionsContainer.test.js b/test/ui/component-repository/components/item/install-controls/ComponentInstallActionsContainer.test.js index 55afd05d7..efac1a3e5 100644 --- a/test/ui/component-repository/components/item/install-controls/ComponentInstallActionsContainer.test.js +++ b/test/ui/component-repository/components/item/install-controls/ComponentInstallActionsContainer.test.js @@ -59,7 +59,7 @@ describe('ComponentInstallActionsContainer', () => { }); it('should dispatch an action if onSave is called', () => { - props.onInstall(); + props.onInstall({}); expect(dispatchMock).toHaveBeenCalled(); });