diff --git a/src/components/SelectionDropdown.demo.js b/src/components/BedFileDropdown.demo.js similarity index 87% rename from src/components/SelectionDropdown.demo.js rename to src/components/BedFileDropdown.demo.js index 1bd33f9e..d69a23b6 100644 --- a/src/components/SelectionDropdown.demo.js +++ b/src/components/BedFileDropdown.demo.js @@ -1,7 +1,7 @@ import Demo, {props as P} from 'react-demo' // See https://github.com/rpominov/react-demo for how to make a demo -import SelectionDropdown from "./SelectionDropdown"; +import BedFileDropdown from "./BedFileDropdown"; // We want to two-way-bind the demo region prop so we use advanced mode and pass the render function. export default ( { // We need to render the component under test using the props in props, and // call update when the component wants to adjust the props. - return { + return { // Bind new value back up to value. // Remember: we get a fake event object with a "target" that has an "id" and a "value" update({value: fakeEvent.target.value}) diff --git a/src/components/SelectionDropdown.js b/src/components/BedFileDropdown.js similarity index 85% rename from src/components/SelectionDropdown.js rename to src/components/BedFileDropdown.js index 9ba99d76..02ab703f 100644 --- a/src/components/SelectionDropdown.js +++ b/src/components/BedFileDropdown.js @@ -19,7 +19,7 @@ import CreatableSelect from 'react-select/creatable'; * // Here e is {"target": {"id": "box1", "value": "b"}} * }}> */ -export class SelectionDropdown extends Component { +export class BedFileDropdown extends Component { render() { // Tweaks to the default react-select styles so that it'll look good with tube maps. const styles = { @@ -49,8 +49,13 @@ export class SelectionDropdown extends Component { }), }; + function getFilename(fullPath) { + const segments = fullPath.split("/"); + return segments[segments.length - 1]; + } + const dropdownOptions = this.props.options.map((option) => ({ - label: option, + label: getFilename(option), value: option, })); @@ -73,19 +78,22 @@ export class SelectionDropdown extends Component { inputId={this.props.inputId} className={this.props.className} value={ - dropdownOptions.find((option) => option.value === this.props.value) || {label: this.props.value , value: this.props.value} + dropdownOptions.find((option) => option.value === this.props.value) || {label: getFilename(this.props.value) , value: this.props.value} } styles={styles} isSearchable={true} onChange={onChange} options={dropdownOptions} + getOptionValue={(o) => { + return o["value"]; + }} openMenuOnClick={dropdownOptions.length < 2000} /> ); } } -SelectionDropdown.propTypes = { +BedFileDropdown.propTypes = { id: PropTypes.string, inputId: PropTypes.string, className: PropTypes.string, @@ -94,11 +102,11 @@ SelectionDropdown.propTypes = { options: PropTypes.array.isRequired, }; -SelectionDropdown.defaultProps = { +BedFileDropdown.defaultProps = { id: undefined, inputId: undefined, className: undefined, value: undefined, }; -export default SelectionDropdown; +export default BedFileDropdown; diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 5bd9aa70..aad55b72 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -10,9 +10,10 @@ import FileUploadFormRow from "./FileUploadFormRow"; import ExampleSelectButtons from "./ExampleSelectButtons"; import RegionInput from "./RegionInput"; import TrackPicker from "./TrackPicker"; -import SelectionDropdown from "./SelectionDropdown"; +import BedFileDropdown from "./BedFileDropdown"; import { parseRegion, stringifyRegion } from "../common.mjs"; + // See src/Types.ts const DATA_SOURCES = config.DATA_SOURCES; @@ -47,7 +48,6 @@ const CLEAR_STATE = { tracks: {}, bedFile: undefined, - dataPath: undefined, region: "", name: undefined, @@ -187,21 +187,19 @@ class HeaderForm extends Component { ); } } - DATA_NAMES = DATA_SOURCES.map((source) => source.name); initState = () => { // Populate state with either viewTarget or the first example let ds = this.props.defaultViewTarget ?? DATA_SOURCES[0]; const bedSelect = ds.bedFile ? ds.bedFile : "none"; - const dataPath = ds.dataPath; if (bedSelect !== "none") { - this.getBedRegions(bedSelect, dataPath); + this.getBedRegions(bedSelect); } for (const key in ds.tracks) { if (ds.tracks[key].trackType === fileTypes.GRAPH) { // Load the paths for any graph tracks console.log("Get path names for track: ", ds.tracks[key]); - this.getPathNames(ds.tracks[key].trackFile, dataPath); + this.getPathNames(ds.tracks[key].trackFile); } } this.setState((state) => { @@ -209,7 +207,6 @@ class HeaderForm extends Component { tracks: ds.tracks, bedFile: ds.bedFile, bedSelect: bedSelect, - dataPath: dataPath, region: ds.region, dataType: ds.dataType, name: ds.name, @@ -310,20 +307,20 @@ class HeaderForm extends Component { } else { json.bedFiles.unshift("none"); - if (this.state.dataPath === "mounted") { + if (this.state.dataType === dataTypes.MOUNTED_FILES) { this.setState((state) => { const bedSelect = json.bedFiles.includes(state.bedSelect) ? state.bedSelect : "none"; if (bedSelect !== "none") { - this.getBedRegions(bedSelect, "mounted"); + this.getBedRegions(bedSelect); } for (const key in state.tracks) { if (state.tracks[key].trackType === fileTypes.GRAPH) { // Load the paths for any graph tracks. // TODO: Do we need to do this now? console.log("Get path names for track: ", state.tracks[key]); - this.getPathNames(state.tracks[key].trackFile, "mounted"); + this.getPathNames(state.tracks[key].trackFile); } } return { @@ -349,7 +346,7 @@ class HeaderForm extends Component { } }; - getBedRegions = async (bedFile, dataPath) => { + getBedRegions = async (bedFile) => { this.setState({ error: null }); try { const json = await fetchAndParse(`${this.props.apiUrl}/getBedRegions`, { @@ -358,7 +355,7 @@ class HeaderForm extends Component { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ bedFile, dataPath }), + body: JSON.stringify({ bedFile }), }); // We need to do all our parsing here, if we expect the catch to catch errors. if (!json.bedRegions || !(json.bedRegions["desc"] instanceof Array)) { @@ -386,7 +383,7 @@ class HeaderForm extends Component { }); }; - getPathNames = async (graphFile, dataPath) => { + getPathNames = async (graphFile) => { this.setState({ error: null }); try { const json = await fetchAndParse(`${this.props.apiUrl}/getPathNames`, { @@ -395,7 +392,7 @@ class HeaderForm extends Component { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ graphFile, dataPath }), + body: JSON.stringify({ graphFile }), }); // We need to do all our parsing here, if we expect the catch to catch errors. let pathNames = json.pathNames; @@ -421,7 +418,6 @@ class HeaderForm extends Component { if (value === dataTypes.FILE_UPLOAD) { const newState = { ...CLEAR_STATE, - dataPath: "upload", dataType: dataTypes.FILE_UPLOAD, error: this.state.error, }; @@ -433,7 +429,6 @@ class HeaderForm extends Component { bedFile: "none", // not sure why we would like to keep the previous selection when changing data sources. What I know is it creates a bug for the regions, where the tubemap tries to read the previous bedFile (e.g. defaulted to example 1), can't find it and raises an error // bedFile: state.bedSelect, - dataPath: "mounted", dataType: dataTypes.MOUNTED_FILES, }; }); @@ -445,10 +440,9 @@ class HeaderForm extends Component { // Find data source whose name matches selection DATA_SOURCES.forEach((ds) => { if (ds.name === value) { - let dataPath = ds.dataPath; let bedSelect = "none"; if (ds.bedFile) { - this.getBedRegions(ds.bedFile, dataPath); + this.getBedRegions(ds.bedFile); bedSelect = ds.bedFile; } else { // Without bedFile, we have no regions @@ -458,14 +452,13 @@ class HeaderForm extends Component { if (ds.tracks[key].trackType === fileTypes.GRAPH) { // Load the paths for any graph tracks. console.log("Get path names for track: ", ds.tracks[key]); - this.getPathNames(ds.tracks[key].trackFile, dataPath); + this.getPathNames(ds.tracks[key].trackFile); } } this.setState({ tracks: ds.tracks, bedFile: ds.bedFile, bedSelect: bedSelect, - dataPath: dataPath, region: ds.region, dataType: dataTypes.BUILT_IN, name: ds.name, @@ -480,7 +473,6 @@ class HeaderForm extends Component { bedFile: this.state.bedFile, name: this.state.name, region: this.state.region, - dataPath: this.state.dataPath, dataType: this.state.dataType, }); @@ -558,7 +550,7 @@ class HeaderForm extends Component { // update path names const graphFile = this.getTrackFile(newTracks, fileTypes.GRAPH, 0); if (graphFile && graphFile !== "none"){ - this.getPathNames(graphFile, this.state.dataPath); + this.getPathNames(graphFile); } }; @@ -569,7 +561,7 @@ class HeaderForm extends Component { this.setState({ [id]: value }); if (value !== "none") { - this.getBedRegions(value, this.state.dataPath); + this.getBedRegions(value); } this.setState({ bedFile: value }); @@ -746,7 +738,7 @@ class HeaderForm extends Component { BED file:   - - - - - ); - } -} - -PathNamesFormRow.propTypes = { - pathSelect: PropTypes.string.isRequired, - pathSelectOptions: PropTypes.array.isRequired, - handleInputChange: PropTypes.func.isRequired, -}; - -export default PathNamesFormRow; diff --git a/src/components/TrackFilePicker.js b/src/components/TrackFilePicker.js index 81d4d804..c1ae681d 100644 --- a/src/components/TrackFilePicker.js +++ b/src/components/TrackFilePicker.js @@ -1,5 +1,8 @@ import PropTypes from "prop-types"; import Select from "react-select"; +import React from "react"; + + /* * A selection dropdown component that select files. @@ -23,12 +26,16 @@ export const TrackFilePicker = ({ }) => { - function onChange(option) { + function mountedOnChange(option) { // update parent state value = option.value; handleInputChange(option.value); } + function getFilename(fullPath) { + const segments = fullPath.split("/"); + return segments[segments.length - 1]; + } const fileOptions = [] // find all file options matching the specified file type @@ -40,7 +47,7 @@ export const TrackFilePicker = ({ // takes in an array of options and maps them into a format { - return o["label"]; + return o["value"]; }} - onChange={onChange} + onChange={mountedOnChange} autoComplete="on" className={className} /> diff --git a/src/config.json b/src/config.json index 116cd3ed..dcbd59b1 100644 --- a/src/config.json +++ b/src/config.json @@ -4,19 +4,19 @@ { "name": "snp1kg-BRCA1", "tracks": [ - {"trackFile": "snp1kg-BRCA1.vg.xg", "trackType": "graph", "trackColorSettings": {"mainPalette": "greys", "auxPalette": "ygreys"}}, - {"trackFile": "NA12878-BRCA1.sorted.gam", "trackType": "read"} + {"trackFile": "exampleData/internal/snp1kg-BRCA1.vg.xg", "trackType": "graph", "trackColorSettings": {"mainPalette": "greys", "auxPalette": "ygreys"}}, + {"trackFile": "exampleData/internal/NA12878-BRCA1.sorted.gam", "trackType": "read"} ], "dataPath": "default", "region": "17:1-100", - "bedFile": "snp1kg-BRCA1.bed", + "bedFile": "exampleData/internal/snp1kg-BRCA1.bed", "dataType": "built-in" }, { "name": "vg \"small\" example", "tracks": [ - {"trackFile": "x.vg.xg", "trackType": "graph"}, - {"trackFile": "x.vg.gbwt", "trackType": "haplotype"} + {"trackFile": "exampleData/x.vg.xg", "trackType": "graph"}, + {"trackFile": "exampleData/x.vg.gbwt", "trackType": "haplotype"} ], "dataPath": "mounted", "dataType": "built-in", @@ -25,31 +25,31 @@ { "name": "cactus", "tracks": [ - {"trackFile": "cactus.vg.xg", "trackType": "graph"}, - {"trackFile": "cactus-NA12879.sorted.gam", "trackType": "read"} + {"trackFile": "exampleData/cactus.vg.xg", "trackType": "graph"}, + {"trackFile": "exampleData/cactus-NA12879.sorted.gam", "trackType": "read"} ], "dataPath": "mounted", - "bedFile": "cactus.bed", + "bedFile": "exampleData/cactus.bed", "region": "ref:1-100", "dataType": "built-in" }, { "name": "cactus multiple reads", "tracks": [ - {"trackFile": "cactus.vg.xg", "trackType": "graph"}, - {"trackFile": "cactus0_10.sorted.gam", "trackType": "read"}, - {"trackFile": "cactus10_20.sorted.gam", "trackType": "read"} + {"trackFile": "exampleData/cactus.vg.xg", "trackType": "graph"}, + {"trackFile": "exampleData/cactus0_10.sorted.gam", "trackType": "read"}, + {"trackFile": "exampleData/cactus10_20.sorted.gam", "trackType": "read"} ], "dataPath": "mounted", - "bedFile": "cactus.bed", + "bedFile": "exampleData/cactus.bed", "region": "ref:1-100", "dataType": "built-in" } ], "vgPath": "", - "dataPath": "./exampleData", - "internalDataPath": "./exampleData/internal/", - "tempDirPath": "./temp", + "dataPath": "exampleData", + "internalDataPath": "exampleData/internal/", + "tempDirPath": "temp", "fetchTimeout": 15, "maxFileSizeBytes": 1000000000, diff --git a/src/end-to-end.test.js b/src/end-to-end.test.js index f221163a..74c77c6c 100644 --- a/src/end-to-end.test.js +++ b/src/end-to-end.test.js @@ -308,7 +308,7 @@ describe("When we wait for it to load", () => { it("produces correct link for view before & after go is pressed", async () => { // First test that after pressing go, the link reflects the dat form const expectedLinkBRCA1 = - "http://localhost?name=snp1kg-BRCA1&tracks[0][trackFile]=snp1kg-BRCA1.vg.xg&tracks[0][trackType]=graph&tracks[0][trackColorSettings][mainPalette]=greys&tracks[0][trackColorSettings][auxPalette]=ygreys&tracks[1][trackFile]=NA12878-BRCA1.sorted.gam&tracks[1][trackType]=read&dataPath=default®ion=17%3A1-100&bedFile=snp1kg-BRCA1.bed&dataType=built-in"; + "http://localhost?name=snp1kg-BRCA1&tracks[0][trackFile]=exampleData%2Finternal%2Fsnp1kg-BRCA1.vg.xg&tracks[0][trackType]=graph&tracks[0][trackColorSettings][mainPalette]=greys&tracks[0][trackColorSettings][auxPalette]=ygreys&tracks[1][trackFile]=exampleData%2Finternal%2FNA12878-BRCA1.sorted.gam&tracks[1][trackType]=read&dataPath=default®ion=17%3A1-100&bedFile=exampleData%2Finternal%2Fsnp1kg-BRCA1.bed&dataType=built-in"; // Set up dropdown await act(async () => { let dropdown = document.getElementById("dataSourceSelect"); @@ -342,7 +342,7 @@ it("produces correct link for view before & after go is pressed", async () => { await clickCopyLink(); const expectedLinkCactus = - "http://localhost?tracks[0][trackFile]=cactus.vg.xg&tracks[0][trackType]=graph&tracks[1][trackFile]=cactus-NA12879.sorted.gam&tracks[1][trackType]=read&bedFile=cactus.bed&name=cactus®ion=ref%3A1-100&dataPath=mounted&dataType=built-in"; + "http://localhost?tracks[0][trackFile]=exampleData%2Fcactus.vg.xg&tracks[0][trackType]=graph&tracks[1][trackFile]=exampleData%2Fcactus-NA12879.sorted.gam&tracks[1][trackType]=read&bedFile=exampleData%2Fcactus.bed&name=cactus®ion=ref%3A1-100&dataType=built-in"; // Make sure link has changed after pressing go expect(fakeClipboard).toEqual(expectedLinkCactus); }); diff --git a/src/server.mjs b/src/server.mjs index 674785bb..ec1b5893 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -338,9 +338,6 @@ async function getChunkedData(req, res, next) { console.log("no BED file provided."); } - let dataPath = pickDataPath(req.body.dataPath); - console.log(`dataPath = ${dataPath}`); - // This will have a conitg, start, end, or a contig, start, distance let parsedRegion; try { @@ -353,11 +350,9 @@ async function getChunkedData(req, res, next) { // check the bed file if this region has been pre-fetched let chunkPath = ""; if (req.withBed) { - // Determine where the BED is, URL or local path. - let bed = isValidURL(bedFile) ? bedFile : path.resolve(dataPath, bedFile); // We need to parse the BED file we have been referred to so we can look up // the pre-parsed chunk. - chunkPath = await getChunkPath(bed, parsedRegion); + chunkPath = await getChunkPath(bedFile, parsedRegion); } // We only want to have one downstream callback chain out of here, and we @@ -378,7 +373,7 @@ async function getChunkedData(req, res, next) { "Graph file does not end in valid extension: " + graphFile ); } - if (!isAllowedPath(`${dataPath}${graphFile}`)) { + if (!isAllowedPath(graphFile)) { throw new BadRequestError("Graph file path not allowed: " + graphFile); } // TODO: Use same variable for check and command line? @@ -391,12 +386,12 @@ async function getChunkedData(req, res, next) { //either push gbz with graph and haplotype or push seperate graph and gbwt file if (graphFile.endsWith(".gbz") && gbwtFile.endsWith(".gbz") && graphFile === gbwtFile) { // use gbz haplotype - vgChunkParams.push("-x", `${dataPath}${graphFile}`); + vgChunkParams.push("-x", graphFile); } else if (!graphFile.endsWith(".gbz") && gbwtFile.endsWith(".gbz")){ throw new BadRequestError("Cannot use gbz as haplotype alone."); } else { // ignoring haplotype from graph file and using haplotype from gbwt file - vgChunkParams.push("--no-embedded-haplotypes", "-x", `${dataPath}${graphFile}`); + vgChunkParams.push("--no-embedded-haplotypes", "-x", graphFile); // double-check that the file is a .gbwt and allowed if (!endsWithExtensions(gbwtFile, HAPLOTYPE_EXTENSIONS)) { @@ -404,18 +399,18 @@ async function getChunkedData(req, res, next) { "GBWT file doesn't end in .gbwt or .gbz: " + gbwtFile ); } - if (!isAllowedPath(`${dataPath}${gbwtFile}`)) { + if (!isAllowedPath(gbwtFile)) { throw new BadRequestError("GBWT file path not allowed: " + gbwtFile); } // Use a GBWT haplotype database - vgChunkParams.push("--gbwt-name", `${dataPath}${gbwtFile}`); + vgChunkParams.push("--gbwt-name", gbwtFile); } } else { // push graph file if (graphFile.endsWith(".gbz")) { - vgChunkParams.push("-x", `${dataPath}${graphFile}`, "--no-embedded-haplotypes"); + vgChunkParams.push("-x", graphFile, "--no-embedded-haplotypes"); } else { - vgChunkParams.push("-x", `${dataPath}${graphFile}`); + vgChunkParams.push("-x", graphFile); } } @@ -424,12 +419,12 @@ async function getChunkedData(req, res, next) { if (!gamFile.endsWith(".gam")) { throw new BadRequestError("GAM file doesn't end in .gam: " + gamFile); } - if (!isAllowedPath(`${dataPath}${gamFile}`)) { + if (!isAllowedPath(gamFile)) { throw new BadRequestError("GAM file path not allowed: " + gamFile); } // Use a GAM index console.log("pushing gam file", gamFile); - vgChunkParams.push("-a", `${dataPath}${gamFile}`, "-g"); + vgChunkParams.push("-a", gamFile, "-g"); } @@ -1070,33 +1065,6 @@ assert( "Scratch path is not acceptable; does it contain .. or //?" ); -// Decide where to pull the data from -// (builtin examples, mounted user data folder or uploaded data). -// Returned path is guaranteed to pass isAllowedPath(). -function pickDataPath(reqDataPath) { - let dataPath; - switch (reqDataPath) { - case "mounted": - dataPath = MOUNTED_DATA_PATH; - break; - case "upload": - dataPath = UPLOAD_DATA_PATH; - break; - case "default": - dataPath = INTERNAL_DATA_PATH; - break; - default: - // User supplied an impermissible option. - throw new BadRequestError("Unrecognized data path type: " + reqDataPath); - } - if (!dataPath.endsWith("/")) { - dataPath = dataPath + "/"; - } - // This path will always be allowed. Caller does not need to check. - assert(isAllowedPath(dataPath)); - return dataPath; -} - api.get("/getFilenames", (req, res) => { console.log("received request for filenames"); const result = { @@ -1107,17 +1075,18 @@ api.get("/getFilenames", (req, res) => { if (isAllowedPath(MOUNTED_DATA_PATH)) { // list files in folder fs.readdirSync(MOUNTED_DATA_PATH).forEach((file) => { + const absPath = path.resolve(MOUNTED_DATA_PATH, file); if (endsWithExtensions(file, GRAPH_EXTENSIONS)) { - result.files.push({"trackFile": file, "trackType": "graph"}); + result.files.push({"trackFile": absPath, "trackType": "graph"}); } if (endsWithExtensions(file, HAPLOTYPE_EXTENSIONS)) { - result.files.push({"trackFile": file, "trackType": "haplotype"}); + result.files.push({"trackFile": absPath, "trackType": "haplotype"}); } if (file.endsWith(".sorted.gam")) { - result.files.push({"trackFile": file, "trackType": "read"}); + result.files.push({"trackFile": absPath, "trackType": "read"}); } if (file.endsWith(".bed")) { - result.bedFiles.push(file); + result.bedFiles.push(absPath); } }); } else { @@ -1139,10 +1108,8 @@ api.post("/getPathNames", (req, res, next) => { pathNames: [], }; - let dataPath = pickDataPath(req.body.dataPath); - // call 'vg paths' to get path name information - const graphFile = `${dataPath}${req.body.graphFile}`; + const graphFile = req.body.graphFile; if (!isAllowedPath(graphFile)) { // Spit back the provided user data in the error, not the generated and @@ -1365,10 +1332,7 @@ api.post("/getBedRegions", (req, res, next) => { }; if (req.body.bedFile) { - let dataPath = pickDataPath(req.body.dataPath); - // Get the path or URL to the actual BED file. - let bed = isValidURL(req.body.bedFile) ? req.body.bedFile : path.resolve(dataPath, req.body.bedFile); - let bed_info = await getBedRegions(bed); + let bed_info = await getBedRegions(req.body.bedFile); result.bedRegions = bed_info; res.json(result); } else {