diff --git a/src/api/reloadConfiguration.js b/src/api/reloadConfiguration.js index 51b5cd011..f238be69b 100644 --- a/src/api/reloadConfiguration.js +++ b/src/api/reloadConfiguration.js @@ -1,5 +1,6 @@ import { makeRequest, METHODS } from '@entando/apimanager'; -import { SUCCESS } from 'test/mocks/reloadConfiguration'; +// eslint-disable-next-line import/no-unresolved, import/extensions +import { SUCCESS, STATUS_RESPONSE } from 'test/mocks/reloadConfiguration'; export const reloadConf = () => ( makeRequest({ @@ -11,5 +12,13 @@ export const reloadConf = () => ( }) ); +export const getReloadStatus = () => ( + makeRequest({ + uri: '/api/reloadConfiguration/status', + method: METHODS.GET, + mockResponse: STATUS_RESPONSE, + useAuthentication: true, + }) +); export default reloadConf; diff --git a/src/locales/en.js b/src/locales/en.js index 1b28feb5a..13c5f1c00 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -732,8 +732,16 @@ export default { 'reloadConfiguration.help': 'The RELOAD CONFIGURATION section allows users to reload the system configuration. This operation is necessary after modifying some parameters.', 'reloadConfiguration.reload.title': 'Reload the configuration', 'reloadConfiguration.reload.confirm': 'Are you sure you want to reload the configuration?', - 'reloadConfiguration.confirm.success': 'The configuration has been reloaded.', + 'reloadConfiguration.confirm.success': 'The configuration has been reloaded successfully.', 'reloadConfiguration.confirm.error': 'Something went wrong while reloading the configuration. Try again in a minute.', + 'reloadConfiguration.confirm.progress': 'Reloading configuration in progress...', + 'reloadConfiguration.confirm.pleaseWait': 'Please wait while the system configuration is being reloaded. This page will update automatically.', + 'reloadConfiguration.confirm.waiting': 'Configuration reload completed with warnings. Some components could not be reloaded properly.', + 'reloadConfiguration.confirm.fail': 'Configuration reload failed. Please check the system logs for more details.', + 'reloadConfiguration.table.beanId': 'Component', + 'reloadConfiguration.table.status': 'Status', + 'reloadConfiguration.bean.status.ok': 'OK', + 'reloadConfiguration.bean.status.ko': 'Error', 'activityStream.newPage': 'created a new page', 'activityStream.editPage': 'edited a new page', 'activityStream.deletePage': 'delete a page', diff --git a/src/locales/it.js b/src/locales/it.js index 3e0ddd6b6..57fb5f719 100644 --- a/src/locales/it.js +++ b/src/locales/it.js @@ -732,8 +732,16 @@ export default { 'reloadConfiguration.help': 'Dalla sezione RICARICA CONFIGURAZIONE è possibile ricaricare la configurazione del sistema. Questa operazione si rende necessaria dopo la modifica di alcuni parametri.', 'reloadConfiguration.reload.title': 'Ricarica la Configurazione', 'reloadConfiguration.reload.confirm': 'Sei sicuro di voler ricaricare la configurazione?', - 'reloadConfiguration.confirm.success': 'La configurazione di sistema è stata ricaricata.', + 'reloadConfiguration.confirm.success': 'La configurazione di sistema è stata ricaricata con successo.', 'reloadConfiguration.confirm.error': 'Non è stato possibile ricaricare la configurazione di sistema', + 'reloadConfiguration.confirm.progress': 'Ricaricamento della configurazione in corso...', + 'reloadConfiguration.confirm.pleaseWait': 'Attendere mentre la configurazione del sistema viene ricaricata. Questa pagina si aggiornerà automaticamente.', + 'reloadConfiguration.confirm.waiting': 'Ricaricamento della configurazione completato con avvisi. Alcuni componenti non sono stati ricaricati correttamente.', + 'reloadConfiguration.confirm.fail': 'Ricaricamento della configurazione non riuscito. Controllare i log di sistema per ulteriori dettagli.', + 'reloadConfiguration.table.beanId': 'Componente', + 'reloadConfiguration.table.status': 'Stato', + 'reloadConfiguration.bean.status.ok': 'OK', + 'reloadConfiguration.bean.status.ko': 'Errore', 'activityStream.newPage': 'ha creato una nuova Pagina', 'activityStream.editPage': 'ha modificato una Pagina', 'activityStream.deletePage': 'ha eliminato una Pagina', diff --git a/src/state/reload-configuration/actions.js b/src/state/reload-configuration/actions.js index 25858af82..842bf5c2f 100644 --- a/src/state/reload-configuration/actions.js +++ b/src/state/reload-configuration/actions.js @@ -1,9 +1,11 @@ import { addToast, addErrors, TOAST_ERROR } from '@entando/messages'; -import { reloadConf } from 'api/reloadConfiguration'; -import { SET_STATUS } from 'state/reload-configuration/types'; +import { reloadConf, getReloadStatus } from 'api/reloadConfiguration'; +import { SET_STATUS, SET_RELOAD_INFO, SET_LOADING } from 'state/reload-configuration/types'; import { history, ROUTE_RELOAD_CONFIRM } from 'app-init/router'; +const POLLING_INTERVAL = 500; // Poll every 2 seconds + export const setStatus = status => ({ type: SET_STATUS, payload: { @@ -11,19 +13,83 @@ export const setStatus = status => ({ }, }); +export const setReloadInfo = (percentage, info) => ({ + type: SET_RELOAD_INFO, + payload: { + percentage, + info, + }, +}); + +export const setLoading = loading => ({ + type: SET_LOADING, + payload: { + loading, + }, +}); + +// Poll the reload status +const pollReloadStatus = (dispatch) => { + const poll = () => { + getReloadStatus().then((response) => { + response.json().then((data) => { + if (response.ok) { + const { status, percentage, info } = data.payload; + dispatch(setStatus(status)); + dispatch(setReloadInfo(percentage, info)); + + // Continue polling if still in progress + if (status === 'progress') { + setTimeout(poll, POLLING_INTERVAL); + } else { + // Reload complete - stop loading + dispatch(setLoading(false)); + } + } else { + dispatch(addErrors(data.errors.map(err => err.message))); + dispatch(setLoading(false)); + } + }); + }).catch(() => { + dispatch(setLoading(false)); + }); + }; + + // Start polling + poll(); +}; + // thunk export const sendReloadConf = () => dispatch => new Promise((resolve) => { + // Set initial loading state and status + dispatch(setLoading(true)); + dispatch(setStatus('progress')); + dispatch(setReloadInfo(0, null)); + reloadConf().then((response) => { response.json().then((data) => { if (response.ok) { - dispatch(setStatus(data.payload)); + const { status, percentage, info } = data.payload; + dispatch(setStatus(status)); + dispatch(setReloadInfo(percentage, info)); + + // Navigate to confirm page history.push(ROUTE_RELOAD_CONFIRM); + + // Always start polling to get the latest status + // This ensures we show progress even if reload is very fast + pollReloadStatus(dispatch); + + resolve(); } else { dispatch(addErrors(data.errors.map(err => err.message))); data.errors.forEach(err => dispatch(addToast(err.message, TOAST_ERROR))); + dispatch(setLoading(false)); + resolve(); } - resolve(); }); - }).catch(() => {}); + }).catch(() => { + dispatch(setLoading(false)); + }); }); diff --git a/src/state/reload-configuration/reducer.js b/src/state/reload-configuration/reducer.js index 597dc03bd..52bb7e99c 100644 --- a/src/state/reload-configuration/reducer.js +++ b/src/state/reload-configuration/reducer.js @@ -1,6 +1,14 @@ -import { SET_STATUS } from 'state/reload-configuration/types'; +import { SET_STATUS, SET_RELOAD_INFO, SET_LOADING } from 'state/reload-configuration/types'; +import { combineReducers } from 'redux'; -export const status = (state = {}, action = {}) => { +const initialState = { + status: null, + percentage: null, + info: null, + loading: false, +}; + +export const status = (state = initialState.status, action = {}) => { switch (action.type) { case SET_STATUS: { return action.payload.status; @@ -9,4 +17,36 @@ export const status = (state = {}, action = {}) => { } }; -export default status; +export const percentage = (state = initialState.percentage, action = {}) => { + switch (action.type) { + case SET_RELOAD_INFO: { + return action.payload.percentage; + } + default: return state; + } +}; + +export const info = (state = initialState.info, action = {}) => { + switch (action.type) { + case SET_RELOAD_INFO: { + return action.payload.info; + } + default: return state; + } +}; + +export const loading = (state = initialState.loading, action = {}) => { + switch (action.type) { + case SET_LOADING: { + return action.payload.loading; + } + default: return state; + } +}; + +export default combineReducers({ + status, + percentage, + info, + loading, +}); diff --git a/src/state/reload-configuration/selectors.js b/src/state/reload-configuration/selectors.js index fff7d2110..980ec9958 100644 --- a/src/state/reload-configuration/selectors.js +++ b/src/state/reload-configuration/selectors.js @@ -1,3 +1,6 @@ // eslint-disable-next-line export const getConfiguration = state => state.configuration; export const getStatus = state => state.configuration.status; +export const getPercentage = state => state.configuration.percentage; +export const getInfo = state => state.configuration.info; +export const getLoading = state => state.configuration.loading; diff --git a/src/state/reload-configuration/types.js b/src/state/reload-configuration/types.js index 7c36e3e21..d66c5f3cf 100644 --- a/src/state/reload-configuration/types.js +++ b/src/state/reload-configuration/types.js @@ -1,2 +1,4 @@ // eslint-disable-next-line export const SET_STATUS = 'reload-configuration/set-status'; +export const SET_RELOAD_INFO = 'reload-configuration/set-reload-info'; +export const SET_LOADING = 'reload-configuration/set-loading'; diff --git a/src/ui/reload-configuration/ReloadConfirm.js b/src/ui/reload-configuration/ReloadConfirm.js index b133f2efc..3cbe32cb0 100644 --- a/src/ui/reload-configuration/ReloadConfirm.js +++ b/src/ui/reload-configuration/ReloadConfirm.js @@ -1,26 +1,109 @@ import React from 'react'; import { Alert } from 'patternfly-react'; +import { Table } from 'react-bootstrap'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -const ReloadConfirm = ({ status }) => { - const alertType = status === 'success' ? 'success' : 'danger'; +const ReloadConfirm = ({ + status, percentage, info, loading, +}) => { + // Determine alert type based on status + let alertType = 'info'; + let messageId = 'reloadConfiguration.confirm.progress'; + + if (status === 'success') { + alertType = 'success'; + messageId = 'reloadConfiguration.confirm.success'; + } else if (status === 'waiting') { + alertType = 'warning'; + messageId = 'reloadConfiguration.confirm.waiting'; + } else if (status === 'fail') { + alertType = 'danger'; + messageId = 'reloadConfiguration.confirm.fail'; + } + + // Render info table if available + const renderInfoTable = () => { + if (!info || Object.keys(info).length === 0) { + return null; + } + + const entries = Object.entries(info); + + return ( +
+ + + + + + + + + + {entries.map(([beanId, errorMessage], index) => { + const hasError = errorMessage && errorMessage.trim().length > 0; + return ( + + + + + + ); + })} + +
# + + + +
{index + 1} + {hasError ? {beanId} : beanId} + + {hasError ? ( + + + : {errorMessage} + + ) : ( + + )} +
+
+ ); + }; + + const isInProgress = status === 'progress' || loading; return (
- + + {isInProgress && percentage !== null && percentage >= 0 && ( + - {percentage}% + )} + {isInProgress && ( +

+ +

+ )} + {renderInfoTable()}
); }; ReloadConfirm.propTypes = { status: PropTypes.string, + percentage: PropTypes.number, + info: PropTypes.objectOf(PropTypes.string), + loading: PropTypes.bool, }; ReloadConfirm.defaultProps = { - status: 'error', + status: null, + percentage: null, + info: null, + loading: false, }; export default ReloadConfirm; diff --git a/src/ui/reload-configuration/ReloadConfirmContainer.js b/src/ui/reload-configuration/ReloadConfirmContainer.js index 7e70f50c0..aa75cddbf 100644 --- a/src/ui/reload-configuration/ReloadConfirmContainer.js +++ b/src/ui/reload-configuration/ReloadConfirmContainer.js @@ -1,10 +1,13 @@ import { connect } from 'react-redux'; -import { getStatus } from 'state/reload-configuration/selectors'; +import { getStatus, getPercentage, getInfo, getLoading } from 'state/reload-configuration/selectors'; import ReloadConfirm from 'ui/reload-configuration/ReloadConfirm'; export const mapStateToProps = state => ({ status: getStatus(state), + percentage: getPercentage(state), + info: getInfo(state), + loading: getLoading(state), }); const ReloadConfirmContainer = connect(mapStateToProps, null)(ReloadConfirm); diff --git a/test/state/reload-configuration/actions.test.js b/test/state/reload-configuration/actions.test.js index a7d4ee7d1..b8e40f50a 100644 --- a/test/state/reload-configuration/actions.test.js +++ b/test/state/reload-configuration/actions.test.js @@ -7,13 +7,13 @@ import { sendReloadConf, } from 'state/reload-configuration/actions'; -import { reloadConf } from 'api/reloadConfiguration'; +import { reloadConf, getReloadStatus } from 'api/reloadConfiguration'; import { mockApi } from 'test/testUtils'; -import { SET_STATUS } from 'state/reload-configuration/types'; +import { SET_STATUS, SET_LOADING, SET_RELOAD_INFO } from 'state/reload-configuration/types'; import { history, ROUTE_RELOAD_CONFIRM } from 'app-init/router'; -import { SUCCESS } from 'test/mocks/reloadConfiguration'; +import { SUCCESS, STATUS_SUCCESS } from 'test/mocks/reloadConfiguration'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); @@ -28,6 +28,11 @@ jest.mock('app-init/router', () => ({ }, })); +jest.mock('api/reloadConfiguration', () => ({ + reloadConf: jest.fn(), + getReloadStatus: jest.fn(), +})); + describe('state/reload-configuration/actions', () => { let store; @@ -38,15 +43,20 @@ describe('state/reload-configuration/actions', () => { it('setStatus() should return a well formed action', () => { const action = setStatus(SUCCESS.status); expect(action).toHaveProperty('type', SET_STATUS); - expect(action.payload).toHaveProperty('status', 'success'); + expect(action.payload).toHaveProperty('status', 'progress'); }); describe('sendReloadConf()', () => { it('when reloadConf succeeds should call post action', (done) => { reloadConf.mockImplementation(mockApi({ payload: SUCCESS })); + getReloadStatus.mockImplementation(mockApi({ payload: STATUS_SUCCESS })); store.dispatch(sendReloadConf()).then(() => { expect(reloadConf).toHaveBeenCalled(); expect(history.push).toHaveBeenCalledWith(ROUTE_RELOAD_CONFIRM); + const actions = store.getActions(); + expect(actions[0]).toHaveProperty('type', SET_LOADING); + expect(actions[1]).toHaveProperty('type', SET_STATUS); + expect(actions[2]).toHaveProperty('type', SET_RELOAD_INFO); done(); }).catch(done.fail); }); @@ -56,9 +66,13 @@ describe('state/reload-configuration/actions', () => { store.dispatch(sendReloadConf()).then(() => { expect(reloadConf).toHaveBeenCalled(); const actions = store.getActions(); - expect(actions).toHaveLength(2); - expect(actions[0]).toHaveProperty('type', ADD_ERRORS); - expect(actions[1]).toHaveProperty('type', ADD_TOAST); + expect(actions).toHaveLength(6); + expect(actions[0]).toHaveProperty('type', SET_LOADING); + expect(actions[1]).toHaveProperty('type', SET_STATUS); + expect(actions[2]).toHaveProperty('type', SET_RELOAD_INFO); + expect(actions[3]).toHaveProperty('type', ADD_ERRORS); + expect(actions[4]).toHaveProperty('type', ADD_TOAST); + expect(actions[5]).toHaveProperty('type', SET_LOADING); done(); }).catch(done.fail); }); diff --git a/test/test/mocks/reloadConfiguration.js b/test/test/mocks/reloadConfiguration.js index d29e6579b..0c12acfa1 100644 --- a/test/test/mocks/reloadConfiguration.js +++ b/test/test/mocks/reloadConfiguration.js @@ -1,7 +1,45 @@ export const SUCCESS = { - status: 'success', + status: 'progress', + percentage: 0, + info: null, }; export const ERROR = { - status: 'error', + status: 'fail', + percentage: null, + info: { + RELOAD_THREAD: 'Error starting reload thread', + }, +}; + +export const STATUS_RESPONSE = { + status: 'success', + percentage: null, + info: null, +}; + +export const STATUS_IN_PROGRESS = { + status: 'progress', + percentage: 50, + info: { + 'DataSourceManager': '', + 'ConfigManager': '', + 'PageManager': '', + }, +}; + +export const STATUS_SUCCESS = { + status: 'success', + percentage: null, + info: null, +}; + +export const STATUS_WARNING = { + status: 'waiting', + percentage: null, + info: { + 'DataSourceManager': '', + 'ConfigManager': 'Error reloading configuration', + 'PageManager': '', + }, }; diff --git a/test/ui/reload-configuration/ReloadConfirm.test.js b/test/ui/reload-configuration/ReloadConfirm.test.js index c67c4de12..c2b19229a 100644 --- a/test/ui/reload-configuration/ReloadConfirm.test.js +++ b/test/ui/reload-configuration/ReloadConfirm.test.js @@ -4,7 +4,9 @@ import { shallow } from 'enzyme'; import ReloadConfirm from 'ui/reload-configuration/ReloadConfirm'; const SUCCESS_STATUS = 'success'; -const ERROR_STATUS = 'error'; +const WAITING_STATUS = 'waiting'; +const FAIL_STATUS = 'fail'; +const PROGRESS_STATUS = 'progress'; describe('ReloadConfirm', () => { let component; @@ -15,10 +17,10 @@ describe('ReloadConfirm', () => { }); describe('if "status" prop is not defined', () => { - it('renders an Alert of type "danger"', () => { + it('renders an Alert of type "info"', () => { component = shallow(); const alert = component.find('Alert'); - expect(alert.prop('type')).toEqual('danger'); + expect(alert.prop('type')).toEqual('info'); }); }); @@ -29,10 +31,22 @@ describe('ReloadConfirm', () => { expect(alert.prop('type')).toEqual('success'); }); - it('renders an Alert of type "danger" if status is not "success"', () => { - component = shallow(); + it('renders an Alert of type "warning" if status is "waiting"', () => { + component = shallow(); + const alert = component.find('Alert'); + expect(alert.prop('type')).toEqual('warning'); + }); + + it('renders an Alert of type "danger" if status is "fail"', () => { + component = shallow(); const alert = component.find('Alert'); expect(alert.prop('type')).toEqual('danger'); }); + + it('renders an Alert of type "info" if status is "progress"', () => { + component = shallow(); + const alert = component.find('Alert'); + expect(alert.prop('type')).toEqual('info'); + }); }); }); diff --git a/test/ui/reload-configuration/ReloadConfirmContainer.test.js b/test/ui/reload-configuration/ReloadConfirmContainer.test.js index 0ffdb3f0b..7c3ffe4de 100644 --- a/test/ui/reload-configuration/ReloadConfirmContainer.test.js +++ b/test/ui/reload-configuration/ReloadConfirmContainer.test.js @@ -7,6 +7,9 @@ const INITIAL_STATE = { jest.mock('state/reload-configuration/selectors', () => ({ getStatus: jest.fn().mockReturnValue('getStatus_result'), + getPercentage: jest.fn().mockReturnValue(50), + getInfo: jest.fn().mockReturnValue({ bean1: '', bean2: 'error' }), + getLoading: jest.fn().mockReturnValue(false), })); describe('ReloadConfirmContainer', () => { @@ -19,5 +22,18 @@ describe('ReloadConfirmContainer', () => { it('maps status property', () => { expect(props).toHaveProperty('status', 'getStatus_result'); }); + + it('maps percentage property', () => { + expect(props).toHaveProperty('percentage', 50); + }); + + it('maps info property', () => { + expect(props).toHaveProperty('info'); + expect(props.info).toEqual({ bean1: '', bean2: 'error' }); + }); + + it('maps loading property', () => { + expect(props).toHaveProperty('loading', false); + }); }); });