diff --git a/cypress/commons/actions/generic/Scenarios.js b/cypress/commons/actions/generic/Scenarios.js index 3d5226f0f..07eb2fdea 100644 --- a/cypress/commons/actions/generic/Scenarios.js +++ b/cypress/commons/actions/generic/Scenarios.js @@ -105,6 +105,14 @@ function switchToScenarioView() { getScenarioViewTab().click(); } +function getErrorBanner() { + return cy.get(GENERIC_SELECTORS.genericComponents.error.errorBanner); +} + +function getDismissErrorButton() { + return cy.get(GENERIC_SELECTORS.genericComponents.error.dismissErrorButton); +} + // Select the scenario with the provided name and id function selectScenario(scenarioName, scenarioId) { const reqName = `requestSelectScenario_${scenarioName}`.replaceAll(' ', ''); @@ -246,4 +254,6 @@ export const Scenarios = { validateScenario, rejectScenario, resetScenarioValidationStatus, + getErrorBanner, + getDismissErrorButton, }; diff --git a/cypress/commons/constants/generic/IdConstants.js b/cypress/commons/constants/generic/IdConstants.js index 6df1ec1e5..44bbbfb5d 100644 --- a/cypress/commons/constants/generic/IdConstants.js +++ b/cypress/commons/constants/generic/IdConstants.js @@ -106,5 +106,9 @@ export const GENERIC_SELECTORS = { basicNumberInput: { input: 'input', }, + error: { + errorBanner: '[data-cy=error-banner]', + dismissErrorButton: '[data-cy=dismiss-error-button]', + }, }, }; diff --git a/cypress/integration/brewery/ErrorScenarioRun.spec.js b/cypress/integration/brewery/ErrorScenarioRun.spec.js new file mode 100644 index 000000000..f92976930 --- /dev/null +++ b/cypress/integration/brewery/ErrorScenarioRun.spec.js @@ -0,0 +1,60 @@ +// Copyright (c) Cosmo Tech. +// Licensed under the MIT license. + +import 'cypress-file-upload'; +import utils from '../../commons/TestUtils'; + +import { DATASET, RUN_TEMPLATE } from '../../commons/constants/brewery/TestConstants'; +import { Downloads, Login, Scenarios, ScenarioManager, ScenarioParameters } from '../../commons/actions'; +import { URL_REGEX } from '../../commons/constants/generic/TestConstants'; + +Cypress.Keyboard.defaults({ + keystrokeDelay: 0, +}); + +const SCENARIO_DATASET = DATASET.BREWERY_ADT; +const SCENARIO_RUN_TEMPLATE = RUN_TEMPLATE.BASIC_TYPES; + +function forgeScenarioName() { + const prefix = 'Scenario - '; + return `${prefix}${utils.randomStr(7)}`; +} + +describe('Displaying error banner on run scenario fail', () => { + before(() => { + Login.login(); + }); + + beforeEach(() => { + Login.relogin(); + }); + + const scenarioNamesToDelete = []; + after(() => { + Downloads.clearDownloadsFolder(); + // Delete all tests scenarios + ScenarioManager.switchToScenarioManager(); + for (const scenarioName of scenarioNamesToDelete) { + ScenarioManager.deleteScenario(scenarioName); + } + }); + it('can display error banner and dismiss it', () => { + const scenarioName = forgeScenarioName(); + scenarioNamesToDelete.push(scenarioName); + Scenarios.createScenario(scenarioName, true, SCENARIO_DATASET, SCENARIO_RUN_TEMPLATE); + ScenarioParameters.getLaunchButton().click(); + ScenarioParameters.checkDontAskAgain(); + ScenarioParameters.getLaunchConfirmButton().click(); + cy.intercept('POST', URL_REGEX.SCENARIO_PAGE_RUN_WITH_ID, { + statusCode: 400, + body: { + title: 'Bad Request', + status: 400, + detail: 'Scenario #scenarioId not found in workspace #W-rXeBwRa0PM in organization #O-gZYpnd27G7', + }, + }); + Scenarios.getErrorBanner().should('be.visible'); + Scenarios.getDismissErrorButton().click(); + Scenarios.getErrorBanner().should('not.exist'); + }); +}); diff --git a/src/components/ScenarioParameters/FileManagementUtils.js b/src/components/ScenarioParameters/FileManagementUtils.js index afda8af3c..09b65da71 100644 --- a/src/components/ScenarioParameters/FileManagementUtils.js +++ b/src/components/ScenarioParameters/FileManagementUtils.js @@ -8,6 +8,8 @@ import WorkspaceService from '../../services/workspace/WorkspaceService'; import { AppInsights } from '../../services/AppInsights'; import { DATASET_ID_VARTYPE } from '../../services/config/ApiConstants'; import { DatasetsUtils, ScenarioParametersUtils } from '../../utils'; +import applicationStore from '../../state/Store.config'; +import { catchNonCriticalErrors } from '../../utils/ApiUtils'; const appInsights = AppInsights.getInstance(); @@ -217,11 +219,8 @@ const prepareToDeleteFile = (setClientFileDescriptorStatus) => { }; const downloadFile = async (datasetId, setClientFileDescriptorStatus) => { - const { error, data } = await DatasetService.findDatasetById(ORGANIZATION_ID, datasetId); - if (error) { - console.error(error); - throw new Error(`Error finding dataset ${datasetId}`); - } else { + try { + const { data } = await DatasetService.findDatasetById(ORGANIZATION_ID, datasetId); const storageFilePath = DatasetsUtils.getStorageFilePathFromDataset(data); if (storageFilePath !== undefined) { setClientFileDescriptorStatus(UPLOAD_FILE_STATUS_KEY.DOWNLOADING); @@ -229,6 +228,8 @@ const downloadFile = async (datasetId, setClientFileDescriptorStatus) => { setClientFileDescriptorStatus(UPLOAD_FILE_STATUS_KEY.READY_TO_DOWNLOAD); } appInsights.trackDownload(); + } catch (error) { + applicationStore.dispatch(catchNonCriticalErrors(error, 'Impossible to download dataset')); } }; @@ -241,7 +242,6 @@ const downloadFileData = async (datasets, datasetId, setClientFileDescriptorStat if (!storageFilePath) { return; } - setClientFileDescriptorStatuses(UPLOAD_FILE_STATUS_KEY.DOWNLOADING, TABLE_DATA_STATUS.DOWNLOADING); const data = await WorkspaceService.downloadWorkspaceFileData(ORGANIZATION_ID, WORKSPACE_ID, storageFilePath); setClientFileDescriptorStatuses(UPLOAD_FILE_STATUS_KEY.READY_TO_DOWNLOAD, TABLE_DATA_STATUS.PARSING); diff --git a/src/layouts/TabLayout/TabLayout.js b/src/layouts/TabLayout/TabLayout.js index 8ee54dc86..104b6f6e4 100644 --- a/src/layouts/TabLayout/TabLayout.js +++ b/src/layouts/TabLayout/TabLayout.js @@ -6,7 +6,7 @@ import { AppBar, Tabs, Tab, Box, makeStyles } from '@material-ui/core'; import { Switch, Route, Link, Redirect, useLocation } from 'react-router-dom'; import PropTypes from 'prop-types'; import { Auth } from '@cosmotech/core'; -import { PrivateRoute, UserInfo, HelpMenu } from '@cosmotech/ui'; +import { PrivateRoute, UserInfo, HelpMenu, ErrorBanner } from '@cosmotech/ui'; import { useTranslation } from 'react-i18next'; import { LANGUAGES, SUPPORT_URL, DOCUMENTATION_URL } from '../../config/AppConfiguration'; import { About } from '../../services/config/Menu'; @@ -71,7 +71,7 @@ const useStyles = makeStyles((theme) => ({ const TabLayout = (props) => { const classes = useStyles(); - const { tabs, authenticated, authorized, signInPath, unauthorizedPath } = props; + const { tabs, authenticated, authorized, signInPath, unauthorizedPath, error, clearMinorErrors } = props; const { t, i18n } = useTranslation(); const location = useLocation(); @@ -86,7 +86,6 @@ const TabLayout = (props) => { aboutTitle: t('genericcomponent.helpmenu.about'), close: t('genericcomponent.dialog.about.button.close'), }; - return ( <> @@ -131,6 +130,7 @@ const TabLayout = (props) => { + {error && } {tabs.map((tab) => ( ({ userId: state.auth.userId, userName: state.auth.userName, userProfilePic: state.auth.profilePic || '', authStatus: state.auth.status, + error: state.application.error, }); -export default connect(mapStateToProps)(TabLayout); +const mapDispatchToProps = { + clearMinorErrors: dispatchClearMinorErrors, +}; +export default connect(mapStateToProps, mapDispatchToProps)(TabLayout); diff --git a/src/services/scenarioRun/ScenarioRunService.js b/src/services/scenarioRun/ScenarioRunService.js index a3d758324..7699bb13e 100644 --- a/src/services/scenarioRun/ScenarioRunService.js +++ b/src/services/scenarioRun/ScenarioRunService.js @@ -5,36 +5,30 @@ import { FileBlobUtils } from '@cosmotech/core'; import { LOG_TYPES } from './ScenarioRunConstants.js'; import { ORGANIZATION_ID } from '../../config/AppInstance'; import { Api } from '../../services/config/Api'; +import applicationStore from '../../state/Store.config'; +import { catchNonCriticalErrors } from '../../utils/ApiUtils'; async function downloadCumulatedLogsFile(lastRun) { try { const fileName = lastRun.scenarioRunId + '_cumulated_logs.txt'; - const { data, status } = await Api.ScenarioRuns.getScenarioRunCumulatedLogs( - ORGANIZATION_ID, - lastRun.scenarioRunId, - { responseType: 'blob' } - ); - if (status !== 200) { - throw new Error(`Error when fetching ${fileName}`); - } + const { data } = await Api.ScenarioRuns.getScenarioRunCumulatedLogs(ORGANIZATION_ID, lastRun.scenarioRunId, { + responseType: 'blob', + }); FileBlobUtils.downloadFileFromData(data, fileName); - } catch (e) { - console.error(e); + } catch (error) { + applicationStore.dispatch(catchNonCriticalErrors(error, 'Impossible to download logs')); } } async function downloadLogsSimpleFile(lastRun) { try { const fileName = lastRun.scenarioRunId + '_simple_logs.json'; - const { data, status } = await Api.ScenarioRuns.getScenarioRunLogs(ORGANIZATION_ID, lastRun.scenarioRunId, { + const { data } = await Api.ScenarioRuns.getScenarioRunLogs(ORGANIZATION_ID, lastRun.scenarioRunId, { responseType: 'blob', }); - if (status !== 200) { - throw new Error(`Error when fetching ${fileName}`); - } FileBlobUtils.downloadFileFromData(data, fileName); - } catch (e) { - console.error(e); + } catch (error) { + applicationStore.dispatch(catchNonCriticalErrors(error, 'Impossible to download logs')); } } diff --git a/src/state/commons/ApplicationConstants.js b/src/state/commons/ApplicationConstants.js index d79cd262b..73bccce58 100644 --- a/src/state/commons/ApplicationConstants.js +++ b/src/state/commons/ApplicationConstants.js @@ -5,4 +5,6 @@ export const APPLICATION_ACTIONS_KEY = { SET_APPLICATION_STATUS: 'SET_APPLICATION_STATUS', GET_ALL_INITIAL_DATA: 'GET_ALL_INITIAL_DATA', + GET_NON_CRITICAL_ERRORS: 'GET_NON_CRITICAL_ERRORS', + CLEAR_ALL_ERRORS: 'CLEAR_ALL_ERRORS', }; diff --git a/src/state/dispatchers/app/ApplicationDispatcher.js b/src/state/dispatchers/app/ApplicationDispatcher.js index b4b9d19dd..a6ad0c9b7 100644 --- a/src/state/dispatchers/app/ApplicationDispatcher.js +++ b/src/state/dispatchers/app/ApplicationDispatcher.js @@ -4,6 +4,7 @@ import { APPLICATION_ACTIONS_KEY } from '../../commons/ApplicationConstants'; import { STATUSES } from '../../commons/Constants'; import { WORKSPACE_ID } from '../../../config/AppInstance'; +import { catchNonCriticalErrors } from '../../../utils/ApiUtils'; export const dispatchSetApplicationStatus = (payLoad) => ({ type: APPLICATION_ACTIONS_KEY.SET_APPLICATION_STATUS, @@ -15,3 +16,12 @@ export const dispatchGetAllInitialData = () => ({ status: STATUSES.LOADING, workspaceId: WORKSPACE_ID, }); + +export const dispatchClearMinorErrors = () => ({ + type: APPLICATION_ACTIONS_KEY.CLEAR_ALL_ERRORS, + error: null, +}); + +export const dispatchCatchNonCriticalErrors = (error, commentOnAppBehaviour) => { + return catchNonCriticalErrors(error, commentOnAppBehaviour); +}; diff --git a/src/state/reducers/app/ApplicationReducer.js b/src/state/reducers/app/ApplicationReducer.js index e6d7a5218..1f64ab566 100644 --- a/src/state/reducers/app/ApplicationReducer.js +++ b/src/state/reducers/app/ApplicationReducer.js @@ -11,14 +11,21 @@ export const applicationInitialState = { }; export const applicationReducer = createReducer(applicationInitialState, (builder) => { - builder.addCase(APPLICATION_ACTIONS_KEY.SET_APPLICATION_STATUS, (state, action) => { - state.status = action.status; - if (state.status === STATUSES.ERROR) { - if (action.error) { - state.error = action.error; - } else { - state.error = { title: 'Unknown error', status: null, detail: 'Something went wrong' }; + builder + .addCase(APPLICATION_ACTIONS_KEY.GET_NON_CRITICAL_ERRORS, (state, action) => { + state.error = action.error; + }) + .addCase(APPLICATION_ACTIONS_KEY.CLEAR_ALL_ERRORS, (state) => { + state.error = null; + }) + .addCase(APPLICATION_ACTIONS_KEY.SET_APPLICATION_STATUS, (state, action) => { + state.status = action.status; + if (state.status === STATUSES.ERROR) { + if (action.error) { + state.error = action.error; + } else { + state.error = { title: 'Unknown error', status: null, detail: 'Something went wrong' }; + } } - } - }); + }); }); diff --git a/src/state/sagas/scenario/CreateScenario/CreateScenarioData.js b/src/state/sagas/scenario/CreateScenario/CreateScenarioData.js index f5c01fb3d..b102be12f 100644 --- a/src/state/sagas/scenario/CreateScenario/CreateScenarioData.js +++ b/src/state/sagas/scenario/CreateScenario/CreateScenarioData.js @@ -7,7 +7,7 @@ import { STATUSES } from '../../../commons/Constants'; import { ORGANIZATION_ID } from '../../../../config/AppInstance'; import { getAllScenariosData } from '../FindAllScenarios/FindAllScenariosData'; import { Api } from '../../../../services/config/Api'; -import { formatParametersFromApi } from '../../../../utils/ApiUtils'; +import { formatParametersFromApi, catchNonCriticalErrors } from '../../../../utils/ApiUtils'; import { AppInsights } from '../../../../services/AppInsights'; const appInsights = AppInsights.getInstance(); @@ -28,8 +28,9 @@ export function* createScenario(action) { status: STATUSES.SUCCESS, scenario: data, }); - } catch (e) { + } catch (error) { // TODO handle error management + yield put(catchNonCriticalErrors(error, 'Scenario not created')); yield put({ type: SCENARIO_ACTIONS_KEY.SET_CURRENT_SCENARIO, status: STATUSES.ERROR, diff --git a/src/state/sagas/scenario/DeleteScenario/DeleteScenario.js b/src/state/sagas/scenario/DeleteScenario/DeleteScenario.js index 31b682d6b..6164c7515 100644 --- a/src/state/sagas/scenario/DeleteScenario/DeleteScenario.js +++ b/src/state/sagas/scenario/DeleteScenario/DeleteScenario.js @@ -1,19 +1,20 @@ // Copyright (c) Cosmo Tech. // Licensed under the MIT license. -import { takeEvery, call } from 'redux-saga/effects'; +import { takeEvery, call, put } from 'redux-saga/effects'; import { SCENARIO_ACTIONS_KEY } from '../../../commons/ScenarioConstants'; import { ORGANIZATION_ID } from '../../../../config/AppInstance'; import { getAllScenariosData } from '../FindAllScenarios/FindAllScenariosData'; import { Api } from '../../../../services/config/Api'; +import { catchNonCriticalErrors } from '../../../../utils/ApiUtils'; export function* deleteScenario(action) { try { const workspaceId = action.workspaceId; yield call(Api.Scenarios.deleteScenario, ORGANIZATION_ID, workspaceId, action.scenarioId); yield call(getAllScenariosData, workspaceId); - } catch (e) { - console.error(e); + } catch (error) { + yield put(catchNonCriticalErrors(error, 'Scenario not deleted')); } } diff --git a/src/state/sagas/scenario/LaunchScenario/LaunchScenario.js b/src/state/sagas/scenario/LaunchScenario/LaunchScenario.js index 40d6725a2..51676e9a2 100644 --- a/src/state/sagas/scenario/LaunchScenario/LaunchScenario.js +++ b/src/state/sagas/scenario/LaunchScenario/LaunchScenario.js @@ -8,6 +8,7 @@ import { SCENARIO_RUN_STATE } from '../../../../services/config/ApiConstants'; import { ORGANIZATION_ID } from '../../../../config/AppInstance'; import { Api } from '../../../../services/config/Api'; import { AppInsights } from '../../../../services/AppInsights'; +import { catchNonCriticalErrors } from '../../../../utils/ApiUtils'; const appInsights = AppInsights.getInstance(); @@ -25,14 +26,14 @@ export function* launchScenario(action) { status: STATUSES.SAVING, scenario: { state: SCENARIO_RUN_STATE.RUNNING }, }); + + // Launch scenario if parameters update succeeded + yield call(Api.ScenarioRuns.runScenario, ORGANIZATION_ID, workspaceId, scenarioId); yield put({ type: SCENARIO_ACTIONS_KEY.UPDATE_SCENARIO, data: { scenarioState: SCENARIO_RUN_STATE.RUNNING, scenarioId: scenarioId, lastRun: null }, }); - // Launch scenario if parameters update succeeded - yield call(Api.ScenarioRuns.runScenario, ORGANIZATION_ID, workspaceId, scenarioId); - // Start backend polling to update the scenario status yield put({ type: SCENARIO_ACTIONS_KEY.START_SCENARIO_STATUS_POLLING, @@ -40,8 +41,13 @@ export function* launchScenario(action) { scenarioId: scenarioId, startTime: runStartTime, }); - } catch (e) { - console.error(e); + } catch (error) { + yield put(catchNonCriticalErrors(error, 'Problem during scenario run')); + yield put({ + type: SCENARIO_ACTIONS_KEY.SET_CURRENT_SCENARIO, + status: STATUSES.ERROR, + scenario: { state: 'Failed' }, + }); } } diff --git a/src/state/sagas/scenario/PollScenarioState/PollScenarioState.js b/src/state/sagas/scenario/PollScenarioState/PollScenarioState.js index fb53413be..91c363056 100644 --- a/src/state/sagas/scenario/PollScenarioState/PollScenarioState.js +++ b/src/state/sagas/scenario/PollScenarioState/PollScenarioState.js @@ -7,6 +7,8 @@ import { ORGANIZATION_ID } from '../../../../config/AppInstance'; import { Api } from '../../../../services/config/Api'; import { SCENARIO_STATUS_POLLING_DELAY } from '../../../../config/AppConfiguration'; import { AppInsights } from '../../../../services/AppInsights'; +import { catchNonCriticalErrors } from '../../../../utils/ApiUtils'; +import { STATUSES } from '../../../commons/Constants'; const appInsights = AppInsights.getInstance(); @@ -50,8 +52,13 @@ export function* pollScenarioState(action) { } // Wait before retrying yield delay(SCENARIO_STATUS_POLLING_DELAY); - } catch (err) { - console.error(err); + } catch (error) { + yield put(catchNonCriticalErrors(error, 'Problem during scenario run')); + yield put({ + type: SCENARIO_ACTIONS_KEY.SET_CURRENT_SCENARIO, + status: STATUSES.ERROR, + scenario: { state: 'Failed' }, + }); // Stop the polling for this scenario yield put(forgeStopPollingAction(action.scenarioId)); } diff --git a/src/state/sagas/scenario/UpdateAndLaunchScenario/UpdateAndLaunchScenario.js b/src/state/sagas/scenario/UpdateAndLaunchScenario/UpdateAndLaunchScenario.js index c0e121ccb..be6027f84 100644 --- a/src/state/sagas/scenario/UpdateAndLaunchScenario/UpdateAndLaunchScenario.js +++ b/src/state/sagas/scenario/UpdateAndLaunchScenario/UpdateAndLaunchScenario.js @@ -4,7 +4,7 @@ import { takeEvery, call, put } from 'redux-saga/effects'; import { SCENARIO_ACTIONS_KEY } from '../../../commons/ScenarioConstants'; import { STATUSES } from '../../../commons/Constants'; -import { formatParametersForApi, formatParametersFromApi } from '../../../../utils/ApiUtils'; +import { catchNonCriticalErrors, formatParametersForApi, formatParametersFromApi } from '../../../../utils/ApiUtils'; import { SCENARIO_RUN_STATE } from '../../../../services/config/ApiConstants'; import { ORGANIZATION_ID } from '../../../../config/AppInstance'; import { Api } from '../../../../services/config/Api'; @@ -17,11 +17,11 @@ export function* updateAndLaunchScenario(action) { const workspaceId = action.workspaceId; const scenarioId = action.scenarioId; const scenarioParameters = action.scenarioParameters; + const runStartTime = new Date().getTime(); try { appInsights.trackScenarioLaunch(); // Update scenario parameters - const runStartTime = new Date().getTime(); yield put({ type: SCENARIO_ACTIONS_KEY.SET_CURRENT_SCENARIO, status: STATUSES.SAVING, @@ -30,10 +30,6 @@ export function* updateAndLaunchScenario(action) { parametersValues: scenarioParameters, }, }); - yield put({ - type: SCENARIO_ACTIONS_KEY.UPDATE_SCENARIO, - data: { scenarioState: SCENARIO_RUN_STATE.RUNNING, scenarioId: scenarioId, lastRun: null }, - }); const { data: updateData } = yield call( Api.Scenarios.updateScenario, ORGANIZATION_ID, @@ -41,17 +37,30 @@ export function* updateAndLaunchScenario(action) { scenarioId, formatParametersForApi(scenarioParameters) ); - updateData.parametersValues = formatParametersFromApi(updateData.parametersValues); - yield put({ type: SCENARIO_ACTIONS_KEY.SET_CURRENT_SCENARIO, status: STATUSES.IDLE, scenario: { state: SCENARIO_RUN_STATE.RUNNING, parametersValues: updateData.parametersValues }, }); + } catch (error) { + yield put(catchNonCriticalErrors(error, "Problem during scenario update : your new parameters aren't saved")); + yield put({ + type: SCENARIO_ACTIONS_KEY.SET_CURRENT_SCENARIO, + status: STATUSES.ERROR, + scenario: { state: 'Failed' }, + }); + return; + } + try { // Launch scenario if parameters update succeeded yield call(Api.ScenarioRuns.runScenario, ORGANIZATION_ID, workspaceId, scenarioId); + yield put({ + type: SCENARIO_ACTIONS_KEY.UPDATE_SCENARIO, + data: { scenarioState: SCENARIO_RUN_STATE.RUNNING, scenarioId: scenarioId, lastRun: null }, + }); + // Start backend polling to update the scenario status yield put({ type: SCENARIO_ACTIONS_KEY.START_SCENARIO_STATUS_POLLING, @@ -59,12 +68,12 @@ export function* updateAndLaunchScenario(action) { scenarioId: scenarioId, startTime: runStartTime, }); - } catch (e) { - console.error(e); + } catch (error) { + yield put(catchNonCriticalErrors(error, 'Problem during scenario run')); yield put({ type: SCENARIO_ACTIONS_KEY.SET_CURRENT_SCENARIO, status: STATUSES.ERROR, - scenario: null, + scenario: { state: 'Failed' }, }); } } diff --git a/src/utils/ApiUtils.js b/src/utils/ApiUtils.js index 761236ee0..4e51936bb 100644 --- a/src/utils/ApiUtils.js +++ b/src/utils/ApiUtils.js @@ -6,6 +6,7 @@ import { VAR_TYPES_TO_STRING_FUNCTIONS } from './scenarioParameters/ConversionTo import { VAR_TYPES_FROM_STRING_FUNCTIONS } from './scenarioParameters/ConversionFromString.js'; import { ConfigUtils } from './ConfigUtils'; import { SCENARIO_PARAMETERS_CONFIG } from '../config/ScenarioParameters'; +import { APPLICATION_ACTIONS_KEY } from '../state/commons/ApplicationConstants'; const clone = rfdc(); @@ -37,3 +38,19 @@ export function formatParametersFromApi(parameters) { const newParams = _formatParameters(parameters, VAR_TYPES_FROM_STRING_FUNCTIONS); return newParams; } + +// Catch non-critical errors to display in error banner +export function catchNonCriticalErrors(error, commentOnAppBehaviour) { + return { + type: APPLICATION_ACTIONS_KEY.GET_NON_CRITICAL_ERRORS, + error: { + title: + error.response?.message || error.response?.data?.title || navigator.onLine + ? 'Unknown error' + : 'Network problem, please check your internet connexion', + detail: error.response?.data?.detail || '', + status: error.response?.data?.status || '', + comment: commentOnAppBehaviour, + }, + }; +} diff --git a/src/views/Scenario/Scenario.js b/src/views/Scenario/Scenario.js index 8410e8e9f..b187b268c 100644 --- a/src/views/Scenario/Scenario.js +++ b/src/views/Scenario/Scenario.js @@ -39,6 +39,7 @@ const Scenario = (props) => { const { setScenarioValidationStatus, + setCurrentScenario, currentScenario, scenarioList, findScenarioById, @@ -51,6 +52,7 @@ const Scenario = (props) => { updateAndLaunchScenario, launchScenario, reports, + catchNonCriticalErrors, } = props; const workspaceId = workspace.data.id; @@ -68,6 +70,11 @@ const Scenario = (props) => { localStorage.setItem('scenarioParametersAccordionExpanded', accordionSummaryExpanded); }, [accordionSummaryExpanded]); + useEffect(() => { + if (currentScenario.data === null && sortedScenarioList.length > 0) { + setCurrentScenario(sortedScenarioList[0]); + } + }); const expandParametersAndCreateScenario = (workspaceId, scenarioData) => { createScenario(workspaceId, scenarioData); setAccordionSummaryExpanded(true); @@ -103,19 +110,38 @@ const Scenario = (props) => { } const resetScenarioValidationStatus = async () => { - setScenarioValidationStatus(currentScenario.data.id, SCENARIO_VALIDATION_STATUS.LOADING); - await ScenarioService.resetValidationStatus(workspaceId, currentScenario.data.id); - findScenarioById(workspaceId, currentScenario.data.id); + const currentStatus = currentScenario.data.validationStatus; + try { + setScenarioValidationStatus(currentScenario.data.id, SCENARIO_VALIDATION_STATUS.LOADING); + await ScenarioService.resetValidationStatus(workspaceId, currentScenario.data.id); + findScenarioById(workspaceId, currentScenario.data.id); + } catch (error) { + catchNonCriticalErrors(error, 'Impossible to reset validation'); + setScenarioValidationStatus( + currentScenario.data.id, + currentStatus === 'Validated' ? SCENARIO_VALIDATION_STATUS.VALIDATED : SCENARIO_VALIDATION_STATUS.REJECTED + ); + } }; const validateScenario = async () => { - setScenarioValidationStatus(currentScenario.data.id, SCENARIO_VALIDATION_STATUS.LOADING); - await ScenarioService.setScenarioValidationStatusToValidated(workspaceId, currentScenario.data.id); - findScenarioById(workspaceId, currentScenario.data.id); + try { + setScenarioValidationStatus(currentScenario.data.id, SCENARIO_VALIDATION_STATUS.LOADING); + await ScenarioService.setScenarioValidationStatusToValidated(workspaceId, currentScenario.data.id); + findScenarioById(workspaceId, currentScenario.data.id); + } catch (error) { + catchNonCriticalErrors(error, 'Impossible to validate the scenario'); + setScenarioValidationStatus(currentScenario.data.id, SCENARIO_VALIDATION_STATUS.DRAFT); + } }; const rejectScenario = async () => { - setScenarioValidationStatus(currentScenario.data.id, SCENARIO_VALIDATION_STATUS.LOADING); - await ScenarioService.setScenarioValidationStatusToRejected(workspaceId, currentScenario.data.id); - findScenarioById(workspaceId, currentScenario.data.id); + try { + setScenarioValidationStatus(currentScenario.data.id, SCENARIO_VALIDATION_STATUS.LOADING); + await ScenarioService.setScenarioValidationStatusToRejected(workspaceId, currentScenario.data.id); + findScenarioById(workspaceId, currentScenario.data.id); + } catch (error) { + catchNonCriticalErrors(error, 'Impossible to reject the scenario'); + setScenarioValidationStatus(currentScenario.data.id, SCENARIO_VALIDATION_STATUS.DRAFT); + } }; const scenarioValidationStatusLabels = { @@ -306,6 +332,7 @@ const Scenario = (props) => { Scenario.propTypes = { setScenarioValidationStatus: PropTypes.func.isRequired, + setCurrentScenario: PropTypes.func.isRequired, scenarioList: PropTypes.object.isRequired, datasetList: PropTypes.object.isRequired, currentScenario: PropTypes.object.isRequired, @@ -318,6 +345,7 @@ Scenario.propTypes = { updateAndLaunchScenario: PropTypes.func.isRequired, launchScenario: PropTypes.func.isRequired, reports: PropTypes.object.isRequired, + catchNonCriticalErrors: PropTypes.func, }; export default Scenario; diff --git a/src/views/Scenario/index.js b/src/views/Scenario/index.js index df5482ebf..ab52ab18f 100644 --- a/src/views/Scenario/index.js +++ b/src/views/Scenario/index.js @@ -8,8 +8,10 @@ import { dispatchUpdateAndLaunchScenario, dispatchLaunchScenario, dispatchSetScenarioValidationStatus, + dispatchSetCurrentScenario, } from '../../state/dispatchers/scenario/ScenarioDispatcher'; import { dispatchAddDatasetToStore } from '../../state/dispatchers/dataset/DatasetDispatcher'; +import { dispatchCatchNonCriticalErrors } from '../../state/dispatchers/app/ApplicationDispatcher'; const mapStateToProps = (state) => ({ scenarioList: state.scenario.list, @@ -28,6 +30,8 @@ const mapDispatchToProps = { createScenario: dispatchCreateScenario, updateAndLaunchScenario: dispatchUpdateAndLaunchScenario, launchScenario: dispatchLaunchScenario, + setCurrentScenario: dispatchSetCurrentScenario, + catchNonCriticalErrors: dispatchCatchNonCriticalErrors, }; export default connect(mapStateToProps, mapDispatchToProps)(Scenario);