Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle non-valid dataset paths #1129

Merged
Merged
3 changes: 2 additions & 1 deletion cli/server/getDataset.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const setUpGetDatasetHandler = ({datasetsPath}) => {
try {
const availableDatasets = await getAvailable.getAvailableDatasets(datasetsPath);
const info = helpers.interpretRequest(req, datasetsPath);
helpers.extendDataPathsToMatchAvailable(info, availableDatasets);
const redirected = helpers.redirectIfDatapathMatchFound(res, info, availableDatasets);
if (redirected) return;
helpers.makeFetchAddresses(info, datasetsPath, availableDatasets);
await helpers.sendJson(res, info);
} catch (err) {
Expand Down
34 changes: 22 additions & 12 deletions cli/server/getDatasetHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,29 @@ const interpretRequest = (req) => {
};

/**
* Given a request, does the dataset exist?
* In the future, if there is no exact match but a partial one we
* should extend this. E.g. `["flu"]` -> `["flu", "h3n2", "ha", "3y"]`
* In that case, we should utilise `res.redirect` as we do in the nextstrain.org server
* @throws
* Given a request, if the dataset does not exist then either
* (a) redirect to an appropriate dataset if possible & return `true`
* (b) throw.
* If the dataset existed then return `false` as no redirect necessary.
*/
const extendDataPathsToMatchAvailable = (info, availableDatasets) => {
const requestStrToMatch = info.parts.join("/"); // TO DO
/* TODO currently there must be an _exact_ match in the available datasets */
if (!availableDatasets.map((d) => d.request).includes(requestStrToMatch)) {
throw new Error(`${requestStrToMatch} not in available datasets`);
const redirectIfDatapathMatchFound = (res, info, availableDatasets) => {
let matchingDatasets = availableDatasets;
let i;
const matchDatasetRequest = (d) => d.request.split("/")[i] === info.parts[i];
// Filter gradually by path fragment, starting from the root
for (i = 0; i < info.parts.length; i++) {
const newMatchingDatasets = matchingDatasets.filter(matchDatasetRequest);
if (!newMatchingDatasets.length) break;
matchingDatasets = newMatchingDatasets;
}
// If root fragment does cannot be matched, throw
if (!i) throw new Error(`${info.parts.join("/")} not in available datasets`);
// If best match is not equal to path requested, redirect
if (matchingDatasets[0].request !== info.parts.join("/")) {
res.redirect(`./getDataset?prefix=/${matchingDatasets[0].request}`);
return true;
}
/* what we want to do is modify `info.parts` to match the available dataset, if possible */
return false;
};

/**
Expand Down Expand Up @@ -157,7 +167,7 @@ const findAvailableSecondTreeOptions = (currentDatasetUrl, availableDatasetUrls)

module.exports = {
interpretRequest,
extendDataPathsToMatchAvailable,
redirectIfDatapathMatchFound,
makeFetchAddresses,
handleError,
sendJson,
Expand Down
20 changes: 6 additions & 14 deletions src/components/controls/choose-dataset-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ import { controlsWidth } from "../../util/globals";


class ChooseDatasetSelect extends React.Component {
createDataPath(dataset) {
let p = (this.props.choice_tree.length > 0) ? "/" : "";
p += this.props.choice_tree.join("/") + "/" + dataset;
p = p.replace(/\/+/, "/");
return p;
}
changeDataset(newPath) {
// 0 analytics (optional)
analyticsControlsEvent(`change-virus-to-${newPath.replace(/\//g, "")}`);
Expand All @@ -24,22 +18,20 @@ class ChooseDatasetSelect extends React.Component {
}
this.props.dispatch(changePage({path: newPath}));
}
getDatasetOptions() {
return this.props.options ?
this.props.options.map((opt) => ({value: opt, label: opt})) :
{};
}
render() {
const datasetOptions = this.getDatasetOptions();
return (
<div style={{width: controlsWidth, fontSize: 14}}>
<Select
value={this.props.selected}
options={datasetOptions}
options={this.props.options || []}
clearable={false}
searchable={false}
multi={false}
onChange={(opt) => {this.changeDataset(this.createDataPath(opt.value));}}
onChange={(opt) => {
if (opt.value !== this.props.selected) {
this.changeDataset(`/${opt.value}`);
}
}}
/>
</div>
);
Expand Down
39 changes: 14 additions & 25 deletions src/components/controls/choose-dataset.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,39 +38,28 @@ class ChooseDataset extends React.Component {
.replace(/\/$/, '')
.split(":")[0];
const displayedDataset = displayedDatasetString.split("/");
const options = [[]];

this.props.available.datasets.forEach((d) => {
const firstField = d.request.split("/")[0];
if (!options[0].includes(firstField)) {
options[0].push(firstField);
}
});


for (let idx=1; idx<displayedDataset.length; idx++) {
/* going through the fields which comprise the current dataset
in order to create available alternatives for each field */
options[idx] = [];
this.props.available.datasets.forEach((singleAvailableOption) => {
/* if the parents (and their parents etc) of this choice match,
then we add that as a valid option */
const fields = singleAvailableOption.request.split("/");
if (checkEqualityOfArrays(fields, displayedDataset, idx) && options[idx].indexOf(fields[idx]) === -1) {
options[idx].push(fields[idx]);
}
});
}
const options = displayedDataset.map((_, i) =>
Array.from(
new Set(
this.props.available.datasets
.filter((ds) => checkEqualityOfArrays(ds.request.split("/"), displayedDataset, i))
.map((ds) => ds.request.split("/")[i])
)
).map((opt) => ({
value: displayedDataset.slice(0, i).concat(opt).join("/"),
label: opt
}))
);

return (
<>
<SidebarHeader>{t("sidebar:Dataset")}</SidebarHeader>
{options.map((option, optionIdx) => (
<ChooseDatasetSelect
key={option}
key={displayedDataset[optionIdx]}
dispatch={this.props.dispatch}
choice_tree={displayedDataset.slice(0, optionIdx)}
selected={displayedDataset[optionIdx]}
selected={displayedDataset.slice(0, optionIdx + 1).join("/")}
options={option}
/>
))}
Expand Down