diff --git a/screen2.0/next.config.mjs b/screen2.0/next.config.mjs index 705ec33d..ad54aa90 100644 --- a/screen2.0/next.config.mjs +++ b/screen2.0/next.config.mjs @@ -20,6 +20,27 @@ const nextConfig = { fullUrl: true, }, }, + + async redirects() { + return [ + // SCREEN Redirects (Legacy link handling) + { + source: '/geApp', + destination: '/applets/gene-expression', + permanent: true, + }, + { + source: '/index/about', + destination: '/about', + permanent: true, + }, + { + source: '/index/cversions', + destination: '/about', + permanent: true, + }, + ] + }, webpack: (config, { isServer }) => { if (!isServer) { diff --git a/screen2.0/src/app/_biosampleTables/DownloadButton.tsx b/screen2.0/src/app/_biosampleTables/DownloadButton.tsx index 0e51e576..fc20d03a 100644 --- a/screen2.0/src/app/_biosampleTables/DownloadButton.tsx +++ b/screen2.0/src/app/_biosampleTables/DownloadButton.tsx @@ -1,8 +1,8 @@ -import { useState, useEffect } from "react"; -import { CircularProgressProps, Box, CircularProgress, Typography, IconButton, Tooltip } from "@mui/material"; -import { Close, Download } from "@mui/icons-material"; -import { downloadTSV } from "../downloads/utils"; +import { useState } from "react"; +import { Box, IconButton, Tooltip } from "@mui/material"; +import { Download } from "@mui/icons-material"; import { BiosampleData } from "./types"; +import Link from "next/link"; export type DownloadButtonProps = { row: BiosampleData @@ -22,7 +22,6 @@ const fetchFileSize = async (url: string, setFileSize: React.Dispatch({ row, downloadType }: DownloadButtonProps) => { - const [progress, setProgress] = useState(null) //for progress wheel - const [hover, setHover] = useState(false) //for tracking if user is hovering over progress wheel - const [controller, setController] = useState(null); //used to hold an AbortController created in handleDL, which allows aborting download const [fileSize, setFileSize] = useState(null) let url: string let fileName: string switch (downloadType) { case "dnase": - url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.dnase}-${row.dnase_signal}.txt` - fileName = `${row.dnase}-${row.dnase_signal}.txt` + url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.dnase}-${row.dnase_signal}.tsv` + fileName = `${row.dnase}-${row.dnase_signal}.tsv` break case "h3k4me3": - url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.h3k4me3}-${row.h3k4me3_signal}.txt` - fileName = `${row.h3k4me3}-${row.h3k4me3_signal}.txt` + url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.h3k4me3}-${row.h3k4me3_signal}.tsv` + fileName = `${row.h3k4me3}-${row.h3k4me3_signal}.tsv` break case "h3k27ac": - url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.h3k27ac}-${row.h3k27ac_signal}.txt` - fileName = `${row.h3k27ac}-${row.h3k27ac_signal}.txt` + url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.h3k27ac}-${row.h3k27ac_signal}.tsv` + fileName = `${row.h3k27ac}-${row.h3k27ac_signal}.tsv` break case "ctcf": - url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.ctcf}-${row.ctcf_signal}.txt` - fileName = `${row.ctcf}-${row.ctcf_signal}.txt` + url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.ctcf}-${row.ctcf_signal}.tsv` + fileName = `${row.ctcf}-${row.ctcf_signal}.tsv` break case "atac": - url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.atac}-${row.atac_signal}.txt` - fileName = `${row.atac}-${row.atac_signal}.txt` + url = `https://downloads.wenglab.org/Registry-V4/SCREEN/Signal-Files/${row.atac}-${row.atac_signal}.tsv` + fileName = `${row.atac}-${row.atac_signal}.tsv` break case "celltypeccres": { const signalIDs = [ @@ -74,153 +70,24 @@ export const DownloadButton = ({ row, downloadType }: Downloa } } - useEffect(() => { - return () => { - // Cleanup: Abort the fetch request if the component is unmounted - if (controller) { - controller.abort(); - } - }; - }, [controller]); - - const handleAbort = () => { - // Trigger the abort signal - if (controller) { - controller.abort(); - } - }; - - function CircularProgressWithLabel( - props: CircularProgressProps & { value: number }, - ) { - return ( - - - - {`${Math.round(props.value)}%`} - - - ); - } - const handleSetHover = (isHovered: boolean) => { - setHover(isHovered) - if (isHovered && !fileSize){ + if (isHovered && !fileSize) { fetchFileSize(url, setFileSize) } } if (row[downloadType] || (downloadType === "celltypeccres" && (row.dnase || row.ctcf || row.h3k27ac || row.h3k4me3))) { - - - const handleDL = async () => { - // Cleanup previous controller if any - if (controller) { - controller.abort(); - } - - // Create a new AbortController - const newController = new AbortController(); - setController(newController); - - // Create a progress callback function - const handleProgress = (progress) => { - setProgress((progress.loaded / progress.total) * 100); - }; - - try { - const response = await fetch(url, { - signal: newController.signal, // Pass the signal to the fetch request - }); - - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - - const reader = response.body!.getReader(); - const contentLength = +response.headers.get('Content-Length')!; - - let receivedLength = 0; - const chunks = []; - - const read = async () => { - const { done, value } = await reader.read(); - - if (done) { - return; - } - - receivedLength += value!.length; - chunks.push(value!); - - handleProgress({ loaded: receivedLength, total: contentLength }); - - // Continue reading the next chunk unless aborted - if (!newController.signal.aborted) { - return read(); - } - }; - - await read(); - - if (!newController.signal.aborted) { - const dataArray = new Uint8Array(receivedLength); - let position = 0; - for (const chunk of chunks) { - dataArray.set(chunk, position); - position += chunk.length; - } - - const dataString = new TextDecoder('utf-8').decode(dataArray); - - downloadTSV(dataString, fileName); - } - } catch (error) { - if (error.name !== 'AbortError') { - window.alert('Download failed:' + error); - } - } finally { - setController(null); - setProgress(null); - } - }; return ( handleSetHover(true)} onMouseLeave={() => handleSetHover(false)} > - {progress ? - hover ? - - - - : - - : - - - - - - } + + + + + ) } else return null diff --git a/screen2.0/src/app/not-found.tsx b/screen2.0/src/app/not-found.tsx new file mode 100644 index 00000000..59c624aa --- /dev/null +++ b/screen2.0/src/app/not-found.tsx @@ -0,0 +1,35 @@ +'use client' + +import { Button, Typography, Stack } from '@mui/material' +import Link from 'next/link' + +export default function NotFound() { + return ( + + + 404 + + + Page Not Found + + + Sorry, the page you’re looking for doesn’t exist or has been moved. + + + If you followed an old link or have this page bookmarked, feel free to contact us and let us know + + + + + + + ) +} diff --git a/screen2.0/src/app/search/_ccredetails/tfintersection.tsx b/screen2.0/src/app/search/_ccredetails/tfintersection.tsx index fc1d1c34..45486b80 100644 --- a/screen2.0/src/app/search/_ccredetails/tfintersection.tsx +++ b/screen2.0/src/app/search/_ccredetails/tfintersection.tsx @@ -6,7 +6,7 @@ import { useQuery } from "@apollo/client" import { TF_INTERSECTION_QUERY, CRE_TF_DCC_QUERY } from "./queries" import Grid from "@mui/material/Grid2" import { DataTable, DataTableColumn } from "@weng-lab/psychscreen-ui-components" -import { LoadingMessage } from "../../../common/lib/utility" +import { CreateLink, LoadingMessage } from "../../../common/lib/utility" type TFBindData = { name: string; @@ -75,7 +75,7 @@ export const TfIntersection: React.FC<{ assembly: string; coordinates: { chromos totalmap[x.target.name] = x.counts.total }) - let tableData: TFBindData[] = + const tableData: TFBindData[] = data && Object.keys(peakmap).map((k) => { return { @@ -91,9 +91,13 @@ export const TfIntersection: React.FC<{ assembly: string; coordinates: { chromos header: "Factor", value: (row) => row.name, render: (row) => ( - - - + e.stopPropagation()} + /> ), }, { @@ -154,13 +158,20 @@ export const TfIntersection: React.FC<{ assembly: string; coordinates: { chromos header: "experiment/file", value: (row) => row.expID, render: (row) => ( - - - + e.stopPropagation()} + /> + // + // + // ), }, ]} diff --git a/screen2.0/src/app/search/page.tsx b/screen2.0/src/app/search/page.tsx index 8d0dcaa5..794cd6a6 100644 --- a/screen2.0/src/app/search/page.tsx +++ b/screen2.0/src/app/search/page.tsx @@ -303,6 +303,8 @@ export default function Search({ searchParams }: { searchParams: { [key: string] //No human or mouse genes have "chr" followed by a number, so safe to check this way if (/chr\d+/.test(encodeInput)) searchType = "region" + //handle rs1 edge case in case user searches in lowercase + else if (encodeInput.toLowerCase() === "rs1") searchType = "gene" //check for "rs" followed by number. Genes RS1 and Rs1 exist, but lowercase r is differentiator else if (/rs\d+/.test(encodeInput)) searchType = "snp" //check for beginning of cCRE accession. No Genes start with these diff --git a/screen2.0/src/common/lib/utility.tsx b/screen2.0/src/common/lib/utility.tsx index 7644972d..b7221c05 100644 --- a/screen2.0/src/common/lib/utility.tsx +++ b/screen2.0/src/common/lib/utility.tsx @@ -16,10 +16,11 @@ export const CreateLink: React.FC<{ linkArg?: string, label: string, showExternalIcon?: boolean, + onClick?: React.MouseEventHandler variant?: OverridableStringUnion, textColor?: string, underline?: "none" | "always" | "hover" }> = (props) => { const link = props.linkPrefix + (props.linkArg ?? "") return ( - + {props.label} {props.showExternalIcon && }