mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-04 14:38:12 +08:00
Improve front-end HTTP error handling (#397)
* UI for HTTP error handing in diffexp route * HTTP error handling for other routes * clean up toasters
This commit is contained in:
+60
-78
@@ -5,7 +5,8 @@ import { Universe, kvCache } from "../util/stateManager";
|
|||||||
import {
|
import {
|
||||||
catchErrorsWrap,
|
catchErrorsWrap,
|
||||||
doJsonRequest,
|
doJsonRequest,
|
||||||
rangeEncodeIndices
|
rangeEncodeIndices,
|
||||||
|
dispatchNetworkErrorMessageToUser
|
||||||
} from "../util/actionHelpers";
|
} from "../util/actionHelpers";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -53,58 +54,12 @@ const doInitialDataLoad = () =>
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: "initial data load error", error });
|
dispatch({ type: "initial data load error", error });
|
||||||
}
|
}
|
||||||
});
|
}, true);
|
||||||
|
|
||||||
// XXX TODO - this is the old code for doing a regraph. Preserving it solely
|
|
||||||
// until we port to 0.2 API. The new UX for regraph can't be implemented on
|
|
||||||
// the 0.1 API (doesn't allow for re-layout on arbitrary sets of cells), so just
|
|
||||||
// punting for now. See ticket #88
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// /* SELECT */
|
|
||||||
// const regraph = () => {
|
|
||||||
// return (dispatch, getState) => {
|
|
||||||
// dispatch({ type: "regraph started" });
|
|
||||||
//
|
|
||||||
// const state = getState();
|
|
||||||
// const selectedMetadata = {};
|
|
||||||
//
|
|
||||||
// _.each(state.controls.categoricalAsBooleansMap, (options, field) => {
|
|
||||||
// let atLeastOneOptionDeselected = false;
|
|
||||||
//
|
|
||||||
// _.each(options, (isActive, option) => {
|
|
||||||
// if (!isActive) {
|
|
||||||
// atLeastOneOptionDeselected = true;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (atLeastOneOptionDeselected) {
|
|
||||||
// _.each(options, (isActive, option) => {
|
|
||||||
// if (isActive) {
|
|
||||||
// if (selectedMetadata[field]) {
|
|
||||||
// selectedMetadata[field].push(option);
|
|
||||||
// } else if (!selectedMetadata[field]) {
|
|
||||||
// selectedMetadata[field] = [option];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// let uri = new URI();
|
|
||||||
// uri.setSearch(selectedMetadata);
|
|
||||||
// console.log(uri.search(), selectedMetadata);
|
|
||||||
//
|
|
||||||
// dispatch(requestCells(uri.search())).then(res => {
|
|
||||||
// if (res.error) {
|
|
||||||
// dispatch({ type: "regraph error" });
|
|
||||||
// } else {
|
|
||||||
// dispatch({ type: "regraph success" });
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set the view (world) to current selection. Placeholder for an async action
|
||||||
|
which also does re-layout.
|
||||||
|
*/
|
||||||
const regraph = () => (dispatch, getState) => {
|
const regraph = () => (dispatch, getState) => {
|
||||||
const { universe, world, crossfilter } = getState().controls;
|
const { universe, world, crossfilter } = getState().controls;
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -115,6 +70,15 @@ const regraph = () => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Throws
|
||||||
|
const dispatchExpressionErrors = (dispatch, res) => {
|
||||||
|
const msg = `Unexpected HTTP response while fetching expression data ${
|
||||||
|
res.status
|
||||||
|
}, ${res.statusText}`;
|
||||||
|
dispatchNetworkErrorMessageToUser(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Fetch expression vectors for each gene in genes. This is NOT an action
|
Fetch expression vectors for each gene in genes. This is NOT an action
|
||||||
function, but rather a helper to be called from an action helper that
|
function, but rather a helper to be called from an action helper that
|
||||||
@@ -141,8 +105,7 @@ async function _doRequestExpressionData(dispatch, getState, genes) {
|
|||||||
if (genesToFetch.length) {
|
if (genesToFetch.length) {
|
||||||
try {
|
try {
|
||||||
// XXX: TODO - this could be using /data/var rather than /data/obs,
|
// XXX: TODO - this could be using /data/var rather than /data/obs,
|
||||||
// as that would simplify the transformation in
|
// as that would simplify the transformation in convertExpressionRESTv02ToObject
|
||||||
// convertExpressionRESTv02ToObject
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${globals.API.prefix}${globals.API.version}data/obs`,
|
`${globals.API.prefix}${globals.API.version}data/obs`,
|
||||||
{
|
{
|
||||||
@@ -161,6 +124,12 @@ async function _doRequestExpressionData(dispatch, getState, genes) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
|
||||||
|
// WILL throw
|
||||||
|
return dispatchExpressionErrors(dispatch, res);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
expressionData = {
|
expressionData = {
|
||||||
...expressionData,
|
...expressionData,
|
||||||
@@ -201,28 +170,12 @@ function requestSingleGeneExpressionCountsForColoringPOST(gene) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestGeneExpressionCountsPOST = genes => async (dispatch, getState) => {
|
const requestUserDefinedGene = gene => async (dispatch, getState) => {
|
||||||
dispatch({ type: "get expression started" });
|
|
||||||
try {
|
|
||||||
const expressionData = await _doRequestExpressionData(
|
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
genes
|
|
||||||
);
|
|
||||||
return dispatch({
|
|
||||||
type: "get expression success",
|
|
||||||
genes,
|
|
||||||
data: expressionData
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return dispatch({ type: "get expression error", error });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestUserDefinedGene = gene => async dispatch => {
|
|
||||||
dispatch({ type: "request user defined gene started" });
|
dispatch({ type: "request user defined gene started" });
|
||||||
try {
|
try {
|
||||||
const data = await dispatch(requestGeneExpressionCountsPOST([gene]));
|
const data = await await _doRequestExpressionData(dispatch, getState, [
|
||||||
|
gene
|
||||||
|
]);
|
||||||
|
|
||||||
/* then send the success case action through */
|
/* then send the success case action through */
|
||||||
return dispatch({
|
return dispatch({
|
||||||
@@ -237,6 +190,31 @@ const requestUserDefinedGene = gene => async dispatch => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dispatchDiffExpErrors = (dispatch, response) => {
|
||||||
|
switch (response.status) {
|
||||||
|
case 403:
|
||||||
|
dispatchNetworkErrorMessageToUser(
|
||||||
|
"Too many cells selected for differential experesion calculation - please make a smaller selection."
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 501:
|
||||||
|
dispatchNetworkErrorMessageToUser(
|
||||||
|
"Differential expression is not implemented."
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
const msg = `Unexpected differential expression HTTP response ${
|
||||||
|
response.status
|
||||||
|
}, ${response.statusText}`;
|
||||||
|
dispatchNetworkErrorMessageToUser(msg);
|
||||||
|
dispatch({
|
||||||
|
type: "request differential expression error",
|
||||||
|
error: new Error(msg)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
||||||
dispatch,
|
dispatch,
|
||||||
getState
|
getState
|
||||||
@@ -256,7 +234,7 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
const set2ByIndex = rangeEncodeIndices(
|
const set2ByIndex = rangeEncodeIndices(
|
||||||
_.map(set2, s => universe.obsNameToIndexMap[s])
|
_.map(set2, s => universe.obsNameToIndexMap[s])
|
||||||
);
|
);
|
||||||
const diffExpFetch = await fetch(
|
const res = await fetch(
|
||||||
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -273,7 +251,12 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const data = await diffExpFetch.json();
|
|
||||||
|
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
|
||||||
|
return dispatchDiffExpErrors(dispatch, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
// result is [ [varIdx, ...], ... ]
|
// result is [ [varIdx, ...], ... ]
|
||||||
const topNGenes = _.map(data, r => universe.varAnnotations[r[0]].name);
|
const topNGenes = _.map(data, r => universe.varAnnotations[r[0]].name);
|
||||||
|
|
||||||
@@ -281,7 +264,7 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
Kick off secondary action to fetch all of the expression data for the
|
Kick off secondary action to fetch all of the expression data for the
|
||||||
topN expressed genes.
|
topN expressed genes.
|
||||||
*/
|
*/
|
||||||
await dispatch(requestGeneExpressionCountsPOST(topNGenes));
|
await _doRequestExpressionData(dispatch, getState, topNGenes);
|
||||||
|
|
||||||
/* then send the success case action through */
|
/* then send the success case action through */
|
||||||
return dispatch({
|
return dispatch({
|
||||||
@@ -321,7 +304,6 @@ export default {
|
|||||||
regraph,
|
regraph,
|
||||||
resetInterface,
|
resetInterface,
|
||||||
requestSingleGeneExpressionCountsForColoringPOST,
|
requestSingleGeneExpressionCountsForColoringPOST,
|
||||||
requestGeneExpressionCountsPOST,
|
|
||||||
requestDifferentialExpression,
|
requestDifferentialExpression,
|
||||||
requestUserDefinedGene,
|
requestUserDefinedGene,
|
||||||
doInitialDataLoad
|
doInitialDataLoad
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import { Position, Toaster, Intent } from "@blueprintjs/core";
|
import { Position, Toaster, Intent } from "@blueprintjs/core";
|
||||||
|
|
||||||
/** Singleton toaster instance. Create separate instances for different options. */
|
/** Singleton toaster instance. Create separate instances for different options. */
|
||||||
export const ErrorToastTopCenter = Toaster.create({
|
|
||||||
|
const ErrorToastTopCenter = Toaster.create({
|
||||||
className: "recipe-toaster",
|
className: "recipe-toaster",
|
||||||
position: Position.TOP,
|
position: Position.TOP
|
||||||
intent: Intent.WARNING
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
/*
|
||||||
ErrorToastTopCenter
|
A "user" error - eg, bad input
|
||||||
};
|
*/
|
||||||
|
export const postUserErrorToast = message =>
|
||||||
|
ErrorToastTopCenter.show({ message, intent: Intent.WARNING });
|
||||||
|
|
||||||
|
/*
|
||||||
|
a hard network error
|
||||||
|
*/
|
||||||
|
export const postNetworkErrorToast = message =>
|
||||||
|
ErrorToastTopCenter.show({
|
||||||
|
message,
|
||||||
|
timeout: 30000,
|
||||||
|
intent: Intent.DANGER
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Button, Tooltip } from "@blueprintjs/core";
|
|||||||
import HistogramBrush from "../brushableHistogram";
|
import HistogramBrush from "../brushableHistogram";
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
import { ErrorToastTopCenter } from "../framework/toasters";
|
import { postUserErrorToast } from "../framework/toasters";
|
||||||
import ExpressionButtons from "./expressionButtons";
|
import ExpressionButtons from "./expressionButtons";
|
||||||
|
|
||||||
@connect(state => {
|
@connect(state => {
|
||||||
@@ -47,18 +47,13 @@ class GeneExpression extends React.Component {
|
|||||||
const { gene } = this.state;
|
const { gene } = this.state;
|
||||||
|
|
||||||
if (userDefinedGenes.indexOf(gene) !== -1) {
|
if (userDefinedGenes.indexOf(gene) !== -1) {
|
||||||
ErrorToastTopCenter.show({
|
postUserErrorToast("That gene already exists");
|
||||||
message: "That gene already exists"
|
|
||||||
});
|
|
||||||
} else if (userDefinedGenes.length > 15) {
|
} else if (userDefinedGenes.length > 15) {
|
||||||
ErrorToastTopCenter.show({
|
postUserErrorToast(
|
||||||
message:
|
"That's too many genes, you can have at most 15 user defined genes"
|
||||||
"That's too many genes, you can have at most 15 user defined genes"
|
);
|
||||||
});
|
|
||||||
} else if (!_.find(world.varAnnotations, { name: gene })) {
|
} else if (!_.find(world.varAnnotations, { name: gene })) {
|
||||||
ErrorToastTopCenter.show({
|
postUserErrorToast("That doesn't appear to be a valid gene name.");
|
||||||
message: "That doesn't appear to be a valid gene name."
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(actions.requestUserDefinedGene(gene));
|
dispatch(actions.requestUserDefinedGene(gene));
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
/* XXX: cough, cough, ... */
|
||||||
|
import { postNetworkErrorToast } from "../components/framework/toasters";
|
||||||
|
|
||||||
|
/*
|
||||||
|
dispatch an action error to the user. Currently we use
|
||||||
|
async toasts.
|
||||||
|
*/
|
||||||
|
export const dispatchNetworkErrorMessageToUser = message =>
|
||||||
|
postNetworkErrorToast(message);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Catch unexpected errors and make sure we don't lose them!
|
Catch unexpected errors and make sure we don't lose them!
|
||||||
*/
|
*/
|
||||||
export function catchErrorsWrap(fn) {
|
export function catchErrorsWrap(fn, dispatchToUser = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
fn(dispatch, getState).catch(error => {
|
fn(dispatch, getState).catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
if (dispatchToUser) {
|
||||||
|
dispatchNetworkErrorMessageToUser(error.message);
|
||||||
|
}
|
||||||
dispatch({ type: "UNEXPECTED ERROR", error });
|
dispatch({ type: "UNEXPECTED ERROR", error });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -23,7 +35,13 @@ export const doJsonRequest = async url => {
|
|||||||
"Accept-Encoding": "gzip, deflate, br"
|
"Accept-Encoding": "gzip, deflate, br"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
return res.json();
|
if (res.ok && res.headers.get("Content-Type") === "application/json") {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
// else an error
|
||||||
|
const msg = `Unexpected HTTP response ${res.status}, ${res.statusText}`;
|
||||||
|
dispatchNetworkErrorMessageToUser(msg);
|
||||||
|
throw new Error(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user