Skip to content

Commit

Permalink
feat: handle error returned by API
Browse files Browse the repository at this point in the history
  • Loading branch information
esasova committed Jun 3, 2022
1 parent 0267340 commit eedc283
Show file tree
Hide file tree
Showing 18 changed files with 234 additions and 67 deletions.
10 changes: 10 additions & 0 deletions cypress/commons/actions/generic/Scenarios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(' ', '');
Expand Down Expand Up @@ -246,4 +254,6 @@ export const Scenarios = {
validateScenario,
rejectScenario,
resetScenarioValidationStatus,
getErrorBanner,
getDismissErrorButton,
};
4 changes: 4 additions & 0 deletions cypress/commons/constants/generic/IdConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,9 @@ export const GENERIC_SELECTORS = {
basicNumberInput: {
input: 'input',
},
error: {
errorBanner: '[data-cy=error-banner]',
dismissErrorButton: '[data-cy=dismiss-error-button]',
},
},
};
60 changes: 60 additions & 0 deletions cypress/integration/brewery/ErrorScenarioRun.spec.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
12 changes: 6 additions & 6 deletions src/components/ScenarioParameters/FileManagementUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -217,18 +219,17 @@ 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);
await WorkspaceService.downloadWorkspaceFile(ORGANIZATION_ID, WORKSPACE_ID, storageFilePath);
setClientFileDescriptorStatus(UPLOAD_FILE_STATUS_KEY.READY_TO_DOWNLOAD);
}
appInsights.trackDownload();
} catch (error) {
applicationStore.dispatch(catchNonCriticalErrors(error, 'Impossible to download dataset'));
}
};

Expand All @@ -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);
Expand Down
8 changes: 5 additions & 3 deletions src/layouts/TabLayout/TabLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();

Expand All @@ -86,7 +86,6 @@ const TabLayout = (props) => {
aboutTitle: t('genericcomponent.helpmenu.about'),
close: t('genericcomponent.dialog.about.button.close'),
};

return (
<>
<AppBar className={classes.bar}>
Expand Down Expand Up @@ -131,6 +130,7 @@ const TabLayout = (props) => {
</Box>
</AppBar>
<Box className={classes.content}>
{error && <ErrorBanner error={error} clearErrors={clearMinorErrors} />}
<Switch>
{tabs.map((tab) => (
<PrivateRoute
Expand Down Expand Up @@ -158,6 +158,8 @@ TabLayout.propTypes = {
unauthorizedPath: PropTypes.string.isRequired,
userName: PropTypes.string.isRequired,
userProfilePic: PropTypes.string.isRequired,
error: PropTypes.object,
clearMinorErrors: PropTypes.func,
};

export default TabLayout;
7 changes: 6 additions & 1 deletion src/layouts/TabLayout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@

import { connect } from 'react-redux';
import TabLayout from './TabLayout';
import { dispatchClearMinorErrors } from '../../state/dispatchers/app/ApplicationDispatcher';

const mapStateToProps = (state) => ({
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);
26 changes: 10 additions & 16 deletions src/services/scenarioRun/ScenarioRunService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/state/commons/ApplicationConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
10 changes: 10 additions & 0 deletions src/state/dispatchers/app/ApplicationDispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
};
25 changes: 16 additions & 9 deletions src/state/reducers/app/ApplicationReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
}
}
}
});
});
});
5 changes: 3 additions & 2 deletions src/state/sagas/scenario/CreateScenario/CreateScenarioData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions src/state/sagas/scenario/DeleteScenario/DeleteScenario.js
Original file line number Diff line number Diff line change
@@ -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'));
}
}

Expand Down
16 changes: 11 additions & 5 deletions src/state/sagas/scenario/LaunchScenario/LaunchScenario.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -25,23 +26,28 @@ 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,
workspaceId: workspaceId,
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' },
});
}
}

Expand Down
Loading

0 comments on commit eedc283

Please sign in to comment.