diff --git a/.env.development b/.env.development index 25fe55b6..f0fc2042 100644 --- a/.env.development +++ b/.env.development @@ -3,4 +3,5 @@ REACT_APP_FEDERATION_API_SERVER='' REACT_APP_BASE_NAME='' REACT_APP_SITE_LOCATION = '' REACT_APP_HTSGET_SERVER = '' -GENERATE_SOURCEMAP=false +REACT_APP_INGEST_SERVER = '' +GENERATE_SOURCEMAP=false \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 46f55f4c..a294a621 100644 --- a/.eslintrc +++ b/.eslintrc @@ -44,6 +44,9 @@ "no-unused-vars": [ 1, { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_", "ignoreRestSiblings": false } ], diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f44ce857..3f5680fa 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,33 +1,41 @@ ## Ticket(s) +- ## Description +- ## Expected Behaviour +- ## Related Issues (if appropriate) +- ## Screenshots (if appropriate) + ### Before PR ### After PR ## To do/Tickets to be made before merging branch -- + +- ## Types of Change(s) -- [ ] 🪲 Bug fix (non-breaking change that fixes an issue) -- [ ] ✨ New feature (non-breaking change that adds functionality) -- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change) + +- [ ] 🪲 Bug fix (non-breaking change that fixes an issue) +- [ ] ✨ New feature (non-breaking change that adds functionality) +- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change) ## Has it been tested for: -- [ ] My change requires a change to the documentation -- [ ] I have updated the documentation accordingly -- [ ] Prettier linter doesn't return errors -- [ ] Production branch PR browser testing: Chrome, Firefox, Edge, etc. -- [ ] Locally tested -- [ ] Dev server tested -- [ ] Production tested when merging into stable/production branch \ No newline at end of file + +- [ ] My change requires a change to the documentation +- [ ] I have updated the documentation accordingly +- [ ] Prettier linter doesn't return errors +- [ ] Production branch PR browser testing: Chrome, Firefox, Edge, etc. +- [ ] Locally tested +- [ ] Dev server tested +- [ ] Production tested when merging into stable/production branch diff --git a/.github/workflows/candig-dispatch.yml b/.github/workflows/candig-dispatch.yml new file mode 100644 index 00000000..a5df4832 --- /dev/null +++ b/.github/workflows/candig-dispatch.yml @@ -0,0 +1,27 @@ +name: Dispatch +on: + push: + branches: [develop] +jobs: + CanDIG-dispatch: + runs-on: ubuntu-latest + env: + PARENT_REPOSITORY: 'CanDIG/CanDIGv2' + CHECKOUT_BRANCH: 'develop' + PR_AGAINST_BRANCH: 'develop' + OWNER: 'CanDIG' + steps: + - name: Check out repository code + uses: actions/checkout@v3 + - name: Create PR in CanDIGv2 + id: make_pr + uses: jman005/github-action-pr-expanded@v0 + with: + github_token: ${{ secrets.SUBMODULE_PR }} + parent_repository: ${{ env.PARENT_REPOSITORY }} + checkout_branch: ${{ env.CHECKOUT_BRANCH}} + pr_against_branch: ${{ env.PR_AGAINST_BRANCH }} + pr_description: 'PR triggered by update to develop branch on ${{ github.repository }}. Commit hash: ${{ github.sha }}' + owner: ${{ env.OWNER }} + submodule_path: lib/candig-data-portal/candig-data-portal + label: Submodule update diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..c2e26e23 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +name: Lint + +on: + # Trigger the workflow on push or pull request, + # but only for the develop branch + push: + branches: + - develop + pull_request: + branches: + - develop + +# Down scope as necessary via https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token +permissions: + checks: write + contents: write + +jobs: + run-linters: + name: Run linters + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install Node.js dependencies + run: npm ci + + - name: Run linters + uses: wearerequired/lint-action@v2 + with: + eslint: true + prettier: true diff --git a/.prettierrc b/.prettierrc index 2eeeb745..c6905b15 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,6 +5,5 @@ "trailingComma": "none", "tabWidth": 4, "useTabs": false, - "endOfLine": "auto", - "camelcase": "off" + "endOfLine": "auto" } diff --git a/PULL_REQUEST_TEMPLATE/stable_pr_template.md b/PULL_REQUEST_TEMPLATE/stable_pr_template.md index 80357313..1fdb79fe 100644 --- a/PULL_REQUEST_TEMPLATE/stable_pr_template.md +++ b/PULL_REQUEST_TEMPLATE/stable_pr_template.md @@ -1,6 +1,7 @@ ## vX.X.X: Description + Add a summary description of the main user-facing changes made in this release, relative to the last stable release. -- [] Tagged as a release in this repo -- [] Passes integration tests on a development instance -- [] Images pushed to Dockerhub +- [ ] Tagged as a release in this repo +- [ ] Passes integration tests on a development instance +- [ ] Images pushed to Dockerhub diff --git a/package-lock.json b/package-lock.json index 1fa64919..22b5f7fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6663,9 +6663,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001343", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001343.tgz", - "integrity": "sha512-8KeCrAtPMabo/XW14B+R9sZYoClx1n0b+WYgwDKZPtWR3TcdvWzdSy7mPyFEmR5WU1St9v1PW6sdO5dkFOEzfA==", + "version": "1.0.30001535", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001535.tgz", + "integrity": "sha512-48jLyUkiWFfhm/afF7cQPqPjaUmSraEhK4j+FCTJpgnGGEZHqyLe3hmWH7lIooZdSzXL0ReMvHz0vKDoTBsrwg==", "funding": [ { "type": "opencollective", @@ -6674,6 +6674,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -27891,9 +27895,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001343", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001343.tgz", - "integrity": "sha512-8KeCrAtPMabo/XW14B+R9sZYoClx1n0b+WYgwDKZPtWR3TcdvWzdSy7mPyFEmR5WU1St9v1PW6sdO5dkFOEzfA==" + "version": "1.0.30001535", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001535.tgz", + "integrity": "sha512-48jLyUkiWFfhm/afF7cQPqPjaUmSraEhK4j+FCTJpgnGGEZHqyLe3hmWH7lIooZdSzXL0ReMvHz0vKDoTBsrwg==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", diff --git a/package.json b/package.json index df271a86..96ed34f4 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "candig-data-portal", "version": "0.1.0", "private": true, + "homepage": "/portal", "dependencies": { "@candig/igv": "^2.10.5-c.1", "@emotion/cache": "^11.4.0", @@ -30,7 +31,6 @@ "highcharts": "^10.0.0", "highcharts-react-official": "^3.1.0", "history": "^5.0.0", - "js-cookie": "^3.0.5", "material-ui-popup-state": "^1.8.0", "npm": "^8.6.0", "papaparse": "^5.3.2", @@ -97,4 +97,3 @@ "webpack-cli": "^4.9.2" } } - diff --git a/public/index.html b/public/index.html index 806160a8..d72179fa 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,7 @@ diff --git a/src/layout/MainLayout/Header/NotificationSection/NotificationList.js b/src/layout/MainLayout/Header/NotificationSection/NotificationList.js index de2983fe..25a29b56 100644 --- a/src/layout/MainLayout/Header/NotificationSection/NotificationList.js +++ b/src/layout/MainLayout/Header/NotificationSection/NotificationList.js @@ -1,5 +1,3 @@ -import React from 'react'; - import { makeStyles } from '@mui/styles'; import { Avatar, diff --git a/src/layout/MainLayout/Header/ProfileSection/UpgradePlanCard.js b/src/layout/MainLayout/Header/ProfileSection/UpgradePlanCard.js index 1dee2001..9484637d 100644 --- a/src/layout/MainLayout/Header/ProfileSection/UpgradePlanCard.js +++ b/src/layout/MainLayout/Header/ProfileSection/UpgradePlanCard.js @@ -1,5 +1,3 @@ -import React from 'react'; - import { makeStyles } from '@mui/styles'; import { Button, Card, CardContent, Grid, Stack, Typography } from '@mui/material'; diff --git a/src/layout/MainLayout/Header/ProfileSection/index.js b/src/layout/MainLayout/Header/ProfileSection/index.js index 2ec415df..98d3345e 100644 --- a/src/layout/MainLayout/Header/ProfileSection/index.js +++ b/src/layout/MainLayout/Header/ProfileSection/index.js @@ -9,11 +9,9 @@ import { ClickAwayListener, Divider, Grid, - InputAdornment, List, ListItemIcon, ListItemText, - OutlinedInput, Paper, Popper, Typography @@ -29,7 +27,7 @@ import Transitions from 'ui-component/extended/Transitions'; import { SITE } from 'store/constant'; // assets -import { IconLogout, IconSearch, IconSettings } from '@tabler/icons'; +import { IconLogout, IconSettings } from '@tabler/icons'; import User1 from 'assets/images/users/user-round.svg'; import BCGSC from 'assets/images/users/bcgsc.svg'; import UHN from 'assets/images/users/UHN.svg'; @@ -77,6 +75,9 @@ const useStyles = makeStyles((theme) => ({ cardContent: { padding: '16px !important' }, + showOnTop: { + zIndex: 11001 + }, card: { backgroundColor: theme.palette.primary.light, marginBottom: '16px', @@ -108,6 +109,9 @@ const useStyles = makeStyles((theme) => ({ badgeWarning: { backgroundColor: theme.palette.warning.dark, color: '#fff' + }, + usernamePadding: { + paddingBottom: '1em' } })); @@ -118,7 +122,6 @@ const ProfileSection = () => { const theme = useTheme(); const customization = useSelector((state) => state.customization); - const [value, setValue] = React.useState(''); const [selectedIndex] = React.useState(1); const [open, setOpen] = React.useState(false); @@ -185,7 +188,7 @@ const ProfileSection = () => { anchorEl={anchorRef.current} role={undefined} transition - disablePortal + className={classes.showOnTop} popperOptions={{ modifiers: [ { @@ -210,26 +213,10 @@ const ProfileSection = () => { First Name Last Name */} - + {SITE} - setValue(e.target.value)} - placeholder="Search profile options" - startAdornment={ - - - - } - aria-describedby="search-helper-text" - inputProps={{ - 'aria-label': 'weight' - }} - /> diff --git a/src/layout/MainLayout/Header/SearchSection/index.js b/src/layout/MainLayout/Header/SearchSection/index.js index e8de0428..2f9eb180 100644 --- a/src/layout/MainLayout/Header/SearchSection/index.js +++ b/src/layout/MainLayout/Header/SearchSection/index.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; // material-ui import { makeStyles } from '@mui/styles'; diff --git a/src/layout/MainLayout/Sidebar/SidebarContext.js b/src/layout/MainLayout/Sidebar/SidebarContext.js index 557b4aca..d87e6d66 100644 --- a/src/layout/MainLayout/Sidebar/SidebarContext.js +++ b/src/layout/MainLayout/Sidebar/SidebarContext.js @@ -1,5 +1,7 @@ import React from 'react'; +import PropTypes from 'prop-types'; + const DEFAULT_STATE = false; const SidebarReaderContext = React.createContext(DEFAULT_STATE); @@ -20,6 +22,11 @@ export function SidebarProvider(props) { ); } +SidebarProvider.propTypes = { + data: PropTypes.object, + setData: PropTypes.func +}; + /** * Obtain the context reader of the federation sites query. * @returns {Object} a React context of the sidebar DOM component diff --git a/src/layout/MainLayout/index.js b/src/layout/MainLayout/index.js index bc5b0a42..7b7ca87e 100644 --- a/src/layout/MainLayout/index.js +++ b/src/layout/MainLayout/index.js @@ -91,8 +91,6 @@ const MainLayout = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [matchDownMd]); - console.log(leftDrawerOpened); - return ( diff --git a/src/layout/NavMotion.js b/src/layout/NavMotion.js index 0daa894c..f513d5f1 100644 --- a/src/layout/NavMotion.js +++ b/src/layout/NavMotion.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import React from 'react'; // third-party import { motion } from 'framer-motion'; diff --git a/src/menu-items/index.js b/src/menu-items/index.js index 072bdc66..0b14f72b 100644 --- a/src/menu-items/index.js +++ b/src/menu-items/index.js @@ -1,5 +1,6 @@ import clinicalGenomicSearch from './clinicalGenomicSearch'; import summary from './summary'; +// import ingest from './ingest'; // import pages from './pages'; // import utilities from './utilities'; @@ -8,7 +9,7 @@ import summary from './summary'; // ===========================|| MENU ITEMS ||=========================== // const menuItems = { - items: [summary, clinicalGenomicSearch /* pages, utilities, other */] + items: [summary, clinicalGenomicSearch /* , ingest, pages, utilities, other */] }; export default menuItems; diff --git a/src/menu-items/ingest.js b/src/menu-items/ingest.js new file mode 100644 index 00000000..97fdf2a3 --- /dev/null +++ b/src/menu-items/ingest.js @@ -0,0 +1,32 @@ +// assets +import { IconUpload } from '@tabler/icons'; + +// import project config +import config from 'config'; + +// constant +const { basename } = config; + +const icons = { + IconUpload +}; + +// ===========================|| Ingest MENU ITEMS ||=========================== // + +const ingest = { + id: 'ingest', + title: 'Data Ingest', + type: 'group', + children: [ + { + id: 'Data Ingest', + title: 'Data Ingest', + type: 'item', + url: `${basename}/data-ingest`, + icon: icons.IconUpload, + breadcrumbs: false + } + ] +}; + +export default ingest; diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js index 363cabeb..4268863b 100644 --- a/src/routes/MainRoutes.js +++ b/src/routes/MainRoutes.js @@ -16,6 +16,9 @@ const Summary = Loadable(lazy(() => import('views/summary/summary'))); // Clinical & Genomic Search const ClinicalGenomicSearch = Loadable(lazy(() => import('views/clinicalGenomic/clinicalGenomicSearch'))); +// Ingest Portal +// const IngestPortal = Loadable(lazy(() => import('views/ingest/ingest'))); + // Error Pages const ErrorNotFoundPage = Loadable(lazy(() => import('views/errorPages/ErrorNotFoundPage'))); @@ -41,6 +44,10 @@ const MainRoutes = { path: `${basename}/clinicalGenomicSearch`, element: }, + /* { + path: `${basename}/data-ingest`, + element: + }, */ { path: '*', element: diff --git a/src/store/api.js b/src/store/api.js index 999634f8..a4f403ae 100644 --- a/src/store/api.js +++ b/src/store/api.js @@ -5,6 +5,7 @@ export const federation = `${process.env.REACT_APP_FEDERATION_API_SERVER}/v1`; export const BASE_URL = process.env.REACT_APP_CANDIG_SERVER; export const htsget = process.env.REACT_APP_HTSGET_SERVER; export const TYK_URL = process.env.REACT_APP_TYK_SERVER; +export const INGEST_URL = process.env.REACT_APP_INGEST_SERVER; // API Calls /* @@ -194,3 +195,61 @@ export function searchVariantByGene(geneName) { return 'error'; }); } + +export function query(parameters, abort) { + const payload = { + ...parameters + }; + + return fetch(`${federation}/fanout`, { + method: 'post', + signal: abort, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + method: 'GET', + path: 'query', + service: 'query', + payload + }) + }) + .then((response) => { + if (response.ok) { + return response.json(); + } + return []; + }) + .catch((error) => { + console.log('Error:', error); + return 'error'; + }); +} + +/* +Post a clinical data JSON to Katsu + * @param {string}... Name of a gene +*/ +export function ingestClinicalData(data) { + return fetch(`${INGEST_URL}/ingest/clinical_donors`, { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: data + }) + .then((response) => response.json()) + .catch((error) => { + console.log('Error:', error); + return error; + }); +} + +export function ingestGenomicData(data, program_id) { + return fetch(`${INGEST_URL}/ingest/moh_variants/${program_id}`, { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: data + }) + .then((response) => response) + .catch((error) => { + console.log('Error:', error); + return error; + }); +} diff --git a/src/ui-component/DataRow.js b/src/ui-component/DataRow.js new file mode 100644 index 00000000..068540c1 --- /dev/null +++ b/src/ui-component/DataRow.js @@ -0,0 +1,119 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Box, Divider, Grid, Typography } from '@mui/material'; +import { useTheme, makeStyles } from '@mui/styles'; + +function makeField(lab, val) { + return { + label: lab, + value: val + }; +} + +const useStyles = makeStyles((theme) => ({ + rowText: { + color: 'black' + }, + container: { + minHeight: 40, + marginRight: 0 + }, + locked: { + backgroundColor: theme.palette.action.disabledBackground + }, + button: { + // Right-aligned + float: 'right', + marginLeft: 'auto' + }, + divider: { + borderColor: theme.palette.primary.main, + marginTop: '0.25em', + marginBottom: '0.25em' + }, + dataBox: { + maxHeight: 'none', + height: 'fit-content', + paddingTop: '0.75em', + paddingBottom: '0.75em', + marginTop: '3em' + }, + label: { + marginTop: '-3.5em', + paddingBottom: '2em', + color: 'black' + } +})); + +function DataRow(props) { + const { fields, itemSize, rowWidth } = props; + const theme = useTheme(); + const classes = useStyles(); + const primaryField = fields.shift(); + + /* const avatarProps = locked + ? { + // If we're locked out, gray out the avatar + sx: { bgcolor: theme.palette.action.disabled } + } + : {}; */ + + return ( + + + + + {primaryField.label} + + + {primaryField.value} + + + {fields.map((field, idx) => ( + + + + + {field.label} + + + {field.value} + + + + ))} + + + ); +} + +DataRow.defaultProps = { + itemSize: '16px', + rowWidth: '100%' +}; + +DataRow.propTypes = { + fields: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired + }) + ).isRequired, + itemSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + rowWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) +}; + +export { makeField, DataRow }; diff --git a/src/ui-component/PersistentFile.js b/src/ui-component/PersistentFile.js new file mode 100644 index 00000000..b9211bd2 --- /dev/null +++ b/src/ui-component/PersistentFile.js @@ -0,0 +1,60 @@ +import { useEffect, useRef, useState } from 'react'; +import { Alert, TextField } from '@mui/material'; +import PropTypes from 'prop-types'; + +const PersistentFile = ({ file, fileLoader }) => { + // A JSON file input dialog which can maintain its state after unmounting. Optionally validates before upload. + // Calls "fileLoader" with the file object as argument #1 and the JSON data as argument #2. + const [error, setError] = useState(null); + const fileRef = useRef(null); + + async function loadFile(file) { + try { + const data = await file.text().then((data) => JSON.parse(data)); + if (!(data || data !== undefined)) { + console.log(`Error parsing uploaded JSON file ${file.name}`); + setError(`Error parsing uploaded JSON "${file.name}`); + return; + } + fileLoader(file, data); + setError(null); + } catch (error) { + console.log(`Error parsing JSON ${file.name}`); + console.log(error); + setError(`Error parsing JSON "${file.name}": ${error.message}`); + fileRef.current.value = ''; + } + } + + useEffect(() => { + if (file && fileRef.current) { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + fileRef.current.files = dataTransfer.files; + } + }); + + return ( + <> + loadFile(event.target.files[0])} + /> + {error !== null && ( + + {error} + + )} + > + ); +}; + +PersistentFile.propTypes = { + fileLoader: PropTypes.func.isRequired, + file: PropTypes.instanceOf(File) +}; + +export default PersistentFile; diff --git a/src/ui-component/cards/CardSecondaryAction.js b/src/ui-component/cards/CardSecondaryAction.js index 4f0aa9f2..ed5166f0 100644 --- a/src/ui-component/cards/CardSecondaryAction.js +++ b/src/ui-component/cards/CardSecondaryAction.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import React from 'react'; // mui import { useTheme } from '@mui/styles'; diff --git a/src/ui-component/cards/Skeleton/ImagePlaceholder.js b/src/ui-component/cards/Skeleton/ImagePlaceholder.js index 94aef7ee..40f2e50f 100644 --- a/src/ui-component/cards/Skeleton/ImagePlaceholder.js +++ b/src/ui-component/cards/Skeleton/ImagePlaceholder.js @@ -1,5 +1,3 @@ -import React from 'react'; - // mui import Skeleton from '@mui/material/Skeleton'; diff --git a/src/ui-component/cards/Skeleton/PopularCard.js b/src/ui-component/cards/Skeleton/PopularCard.js index d0965147..19ca6f3f 100644 --- a/src/ui-component/cards/Skeleton/PopularCard.js +++ b/src/ui-component/cards/Skeleton/PopularCard.js @@ -1,4 +1,3 @@ -import React from 'react'; // mui import { makeStyles } from '@mui/styles'; import { Card, CardContent, Grid, Skeleton } from '@mui/material'; diff --git a/src/ui-component/cards/Skeleton/TotalGrowthBarChart.js b/src/ui-component/cards/Skeleton/TotalGrowthBarChart.js index a0ea7b92..ab758bb0 100644 --- a/src/ui-component/cards/Skeleton/TotalGrowthBarChart.js +++ b/src/ui-component/cards/Skeleton/TotalGrowthBarChart.js @@ -1,5 +1,3 @@ -import React from 'react'; - // mui import { Card, CardContent, Grid } from '@mui/material'; import Skeleton from '@mui/material/Skeleton'; diff --git a/src/ui-component/extended/Avatar.js b/src/ui-component/extended/Avatar.js index 2e89a895..96891ee9 100644 --- a/src/ui-component/extended/Avatar.js +++ b/src/ui-component/extended/Avatar.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import React from 'react'; // mui import { makeStyles } from '@mui/styles'; diff --git a/src/ui-component/ingest/ClinicalIngest.js b/src/ui-component/ingest/ClinicalIngest.js new file mode 100644 index 00000000..e4857c5b --- /dev/null +++ b/src/ui-component/ingest/ClinicalIngest.js @@ -0,0 +1,151 @@ +import { Button, Grid, Typography } from '@mui/material'; +import PropTypes from 'prop-types'; +import { makeField, DataRow } from 'ui-component/DataRow'; +import { makeStyles } from '@mui/styles'; +import { useEffect, useState } from 'react'; +import { fetchFederation } from 'store/api'; + +const ClinicalIngest = ({ setTab, fileUpload, clinicalData }) => { + // setTab should be a function that sets the tab to the genomic ingest page + + const [authorizedCohorts, setAuthorizedCohorts] = useState([]); + + useEffect(() => { + function fetchPrograms() { + return fetchFederation('v2/discovery/donors/', 'katsu') + .then((result) => { + result.forEach((site) => { + const programs = site.results.discovery_donor; + const fields = []; + Object.keys(programs).forEach((program) => { + const field = [ + makeField('Cohort', program), + makeField('Clinical Patients', programs[program].toString()), + makeField('Read Access', 'Unknown') + ]; + fields.push(field); + }); + setAuthorizedCohorts(fields); + }); + }) + .catch((error) => console.log(error)); + } + fetchPrograms(); + }, []); + + const useStyles = makeStyles({ + titleText: { + color: 'black', + fontSize: '1.5em', + fontFamily: 'Roboto' + }, + bodyText: { + color: 'black', + fontSize: '1em', + fontFamily: 'Catamaran' + }, + buttonEnabled: { + position: 'absolute', + right: '0.2em', + bottom: '0.2em' + }, + buttonDisabled: { + position: 'absolute', + right: '0.2em', + bottom: '0.2em', + backgroundColor: 'grey', + '&:hover': { + backgroundColor: 'grey' + } + } + }); + + const classes = useStyles(); + + return ( + <> + + + + Your authorized cohorts + + {authorizedCohorts.length > 0 ? ( + + {authorizedCohorts.map((fields, index) => ( + + + + ))} + + ) : ( + + No cohorts found. + + )} + + + + + Choose a cohort for validation + + + + + Clinical data: + + + + {fileUpload} + + + + + + + Live Preview Summary + + {clinicalData === undefined ? ( + + Waiting for upload... + + ) : ( + + )} + + + + Validation + + + Waiting for validation... + + + + {clinicalData === undefined ? ( + + Next + + ) : ( + + Next + + )} + > + ); +}; + +ClinicalIngest.propTypes = { + setTab: PropTypes.func.isRequired, + fileUpload: PropTypes.element, + clinicalData: PropTypes.object +}; + +export default ClinicalIngest; diff --git a/src/ui-component/ingest/GenomicIngest.js b/src/ui-component/ingest/GenomicIngest.js new file mode 100644 index 00000000..16e5f24d --- /dev/null +++ b/src/ui-component/ingest/GenomicIngest.js @@ -0,0 +1,92 @@ +import { Button, Grid, Typography } from '@mui/material'; +import PropTypes from 'prop-types'; +import { makeField, DataRow } from 'ui-component/DataRow'; +import { makeStyles } from '@mui/styles'; +import { useEffect, useState } from 'react'; + +const GenomicIngest = ({ beginIngest, fileUpload, clinicalData, genomicData }) => { + const [ingestButtonEnabled, setIngestButtonEnabled] = useState(false); + + const cohort = [ + makeField('Cohort', clinicalData.donors[0].program_id), + makeField('Clinical Patients', clinicalData.donors.length), + makeField('Read Access', '1') + ]; + + const useStyles = makeStyles({ + titleText: { + color: 'black', + fontSize: '1.5em', + fontFamily: 'Roboto' + }, + bodyText: { + color: 'black', + fontSize: '1em', + fontFamily: 'Catamaran' + }, + ingestButton: { + backgroundColor: '#37CA50', + width: '7em', + height: '3em' + }, + ingestButtonDisabled: { + backgroundColor: 'grey', + '&:hover': { + backgroundColor: 'grey' + } + } + }); + const classes = useStyles(); + + useEffect(() => genomicData !== undefined && genomicData !== null && setIngestButtonEnabled(true), [genomicData]); + + return ( + <> + + + + Cohort for ingestion + + + + + + Upload genomic sample info + + + + + Genomic data: + + + {fileUpload} + + + + { + if (ingestButtonEnabled) { + setIngestButtonEnabled(false); + beginIngest(); + } + }} + variant="contained" + disabled={!ingestButtonEnabled} + > + Ingest + + + + > + ); +}; + +GenomicIngest.propTypes = { + beginIngest: PropTypes.func.isRequired, + fileUpload: PropTypes.element, + clinicalData: PropTypes.object, + genomicData: PropTypes.object +}; + +export default GenomicIngest; diff --git a/src/ui-component/ingest/IngestMenu.js b/src/ui-component/ingest/IngestMenu.js new file mode 100644 index 00000000..bc84b3dc --- /dev/null +++ b/src/ui-component/ingest/IngestMenu.js @@ -0,0 +1,170 @@ +import { makeStyles, styled } from '@mui/styles'; +import { Alert, Box, CircularProgress, Grid, Tab, Tabs } from '@mui/material'; +import { useEffect, useState } from 'react'; + +import IngestTabPage from 'ui-component/ingest/IngestTabPage'; +import ClinicalIngest from 'ui-component/ingest/ClinicalIngest'; +import GenomicIngest from 'ui-component/ingest/GenomicIngest'; +import PersistentFile from 'ui-component/PersistentFile'; +import { ingestClinicalData, ingestGenomicData } from 'store/api'; + +const IngestTab = styled(Tab)({ + height: '2.75em', + paddingLeft: '0.77em', + paddingRight: '0.77em', + marginBottom: 0, + borderTopLeftRadius: '0.55em', + borderTopRightRadius: '0.55em', + overflow: 'hidden', + justifyContent: 'flex-start', + alignItems: 'start', + gap: '0.59em', + display: 'flex', + textAlign: 'left', + color: '#686969', + fontSize: '1.5em', + fontFamily: 'Roboto', + fontWeight: '700', + wordWrap: 'break-word', + textTransform: 'none', + verticalAlign: 'center' +}); + +function IngestMenu() { + const IngestStates = { + PENDING: 0, + STARTED_CLINICAL: 1, + STARTED_GENOMIC: 2, + ERROR: 3, + SUCCESS: 4 + }; + const [value, setValue] = useState(0); + const [clinicalFile, setClinicalFile] = useState(undefined); + const [clinicalData, setClinicalData] = useState(undefined); + const [genomicData, setGenomicData] = useState(undefined); + const [genomicFile, setGenomicFile] = useState(undefined); + const [ingestState, setIngestState] = useState(IngestStates.PENDING); + const [ingestError, setIngestError] = useState(''); + + const useStyles = makeStyles({ + tabActive: { + border: '0.125vw #2196F3 solid', + borderBottom: 'none', + background: '#FFFFFF' + }, + tabInactive: { + border: '0.1vw #2196F3 solid', + borderBottom: '0.15vw #2196F3 solid', + background: '#F4FAFF' + } + }); + + const classes = useStyles(); + + function getIngestStatusComponent(status) { + function progressRow(component) { + return ( + + + + + + {component} + + + ); + } + + switch (status) { + case IngestStates.STARTED_CLINICAL: + return progressRow(Ingesting clinical data...); + case IngestStates.SUCCESS: + return Ingest complete!; + case IngestStates.ERROR: + return Ingest encountered the following error: {ingestError}; + case IngestStates.STARTED_GENOMIC: + return progressRow(Clinical ingest complete. Beginning genomic ingest...); + default: + return Nothing to show. (You probably should not be seeing this...); + } + } + + function loadClinicalFile(file, data) { + if (!('donors' in data && Array.isArray(data.donors))) { + throw Error('Donors key not found in clinical file'); + } + setClinicalData(data); + setClinicalFile(file); + } + function loadGenomicFile(file, data) { + setGenomicData(data); + setGenomicFile(file); + } + + function beginIngest() { + console.log('Beginning ingest...'); + setIngestState(IngestStates.STARTED_CLINICAL); + ingestClinicalData(JSON.stringify(clinicalData)).then((result) => { + if (result.response_code === 0) { + setIngestState(IngestStates.STARTED_GENOMIC); + ingestGenomicData(JSON.stringify(genomicData), clinicalData.donors[0].program_id).then((response) => { + if (response.status === 200) { + setIngestState(IngestStates.SUCCESS); + } else { + setIngestError(result.json.result); + } + }); + } else { + setIngestError(result.result); + } + }); + } + + useEffect(() => ingestError && setIngestState(IngestStates.ERROR), [ingestError, IngestStates.ERROR]); + + function getPage(val) { + if (val === 0) + return ( + setValue(1)} + fileUpload={ loadClinicalFile(file, data)} />} + clinicalData={clinicalData} + /> + ); + return ( + beginIngest()} + fileUpload={ loadGenomicFile(file, data)} />} + clinicalData={clinicalData} + genomicData={genomicData} + /> + ); + } + + return ( + + setValue(value)} + variant="fullWidth" + sx={{ width: '100%' }} + TabIndicatorProps={{ + style: { display: 'none' } + }} + centered + > + + + + {getPage(value)} + {ingestState !== IngestStates.PENDING && getIngestStatusComponent(ingestState)} + + ); +} + +export default IngestMenu; diff --git a/src/ui-component/ingest/IngestTabPage.js b/src/ui-component/ingest/IngestTabPage.js new file mode 100644 index 00000000..edf7425b --- /dev/null +++ b/src/ui-component/ingest/IngestTabPage.js @@ -0,0 +1,30 @@ +import { Grid } from '@mui/material'; +import { makeStyles } from '@mui/styles'; +import PropTypes from 'prop-types'; + +const IngestTabPage = ({ children }) => { + const useStyles = makeStyles({ + ingestTabPage: { + position: 'relative', + width: '100%', + height: '100%', + minHeight: 100, + borderBottomLeftRadius: '0.75em', + borderBottomRightRadius: '0.75em', + marginTop: 0, + border: '0.125vw #2196F3 solid', + borderTop: 'none', + padding: '1em' + } + }); + + const classes = useStyles(); + + return {children}; +}; + +IngestTabPage.propTypes = { + children: PropTypes.node +}; + +export default IngestTabPage; diff --git a/src/views/clinical/mcode.js b/src/views/clinical/mcode.js deleted file mode 100644 index b6dd9569..00000000 --- a/src/views/clinical/mcode.js +++ /dev/null @@ -1,639 +0,0 @@ -import * as React from 'react'; - -// npm installs -import ReactJson from 'react-json-view'; -import cancerTypeCSV from '../../assets/data_files/cancer_histological_codes_labels.csv'; -import papa from 'papaparse'; - -// mui -import { useTheme, makeStyles } from '@mui/styles'; -import { DataGrid, GridToolbar } from '@mui/x-data-grid'; -import { Grid, Box } from '@mui/material'; - -// REDUX -import { useSelector, useDispatch } from 'react-redux'; - -// project imports -import MainCard from 'ui-component/cards/MainCard'; -import { fetchFederationClinicalData } from 'store/api'; -import { - subjectColumns, - processMCodeMainData, - processMedicationListData, - processCondtionsListData, - processSexListData, - processCancerTypeListData, - processHistologicalTypeListData -} from 'store/mcode'; -import SingleRowTable from 'ui-component/SingleRowTable'; -import { trackPromise } from 'react-promise-tracker'; -import Stack from '@mui/material/Stack'; -import Divider from '@mui/material/Divider'; -import TableContainer from '@mui/material/TableContainer'; -import Table from '@mui/material/Table'; -import DropDown from '../../ui-component/DropDown'; -import { SearchIndicator } from 'ui-component/LoadingIndicator/SearchIndicator'; - -// Styles -const useStyles = makeStyles({ - dropdownItem: { - background: 'white', - paddingRight: '1.25em', - paddingLeft: '1.25em', - border: 'none', - width: 'fit-content(5em)', - '&:hover': { - background: '#2196f3', - color: 'white' - } - }, - mobileRow: { - width: '800px' - }, - scrollbar: { - scrollbarWidth: 'thin', - '&::-webkit-scrollbar': { - height: '0.4em', - width: '0.4em' - }, - '&::-webkit-scrollbar-track': { - boxShadow: 'inset 0 0 4px rgba(0,0,0,0.00)', - webkitBoxShadow: 'inset 0 0 4px rgba(0,0,0,0.00)' - }, - '&::-webkit-scrollbar-thumb': { - backgroundColor: 'rgba(0,0,0,.1)' - } - } -}); - -function MCodeView() { - const theme = useTheme(); - const classes = useStyles(); - const events = useSelector((state) => state); - const dispatch = useDispatch(); - const clinicalSearch = useSelector((state) => state.customization.clinicalSearch); - const clinicalSearchPatients = useSelector((state) => state.customization.clinicalSearchResultPatients); - - const [isLoading, setIsLoading] = React.useState(true); - const [mcodeData, setMcodeData] = React.useState([]); - const [rows, setRows] = React.useState([]); - const [selectedPatient, setSelectedPatient] = React.useState(''); - const [selectedPatientMobileInfo, setSelectedPatientMobileInfo] = React.useState({}); - const [cancerType, setCancerType] = React.useState([]); - - // Mobile - const [desktopResolution, setdesktopResolution] = React.useState(window.innerWidth > 1200); - const [isListOpen, setListOpen] = React.useState(false); - - // Dropdown patient table open/closed - const [isListOpenMedications, setListOpenMedications] = React.useState(false); - const [isListOpenConditions, setListOpenConditions] = React.useState(false); - const [isListOpenSex, setListOpenSex] = React.useState(false); - const [isListOpenCancerType, setListOpenCancerType] = React.useState(false); - const [isListOpenHistological, setListOpenHistological] = React.useState(false); - - // Dropdown patient table filtering current selection in dropdown - const [selectedMedications, setSelectedMedications] = React.useState(clinicalSearch.clinicalSearchDropDowns.selectedMedications); - const [selectedConditions, setSelectedConditions] = React.useState(clinicalSearch.clinicalSearchDropDowns.selectedConditions); - const [selectedSex, setSelectedSex] = React.useState(clinicalSearch.clinicalSearchDropDowns.selectedSex); - const [selectedCancerType, setSelectedCancerType] = React.useState(clinicalSearch.clinicalSearchDropDowns.selectedCancerType); - const [selectedHistologicalType, setSelectedHistologicalType] = React.useState( - clinicalSearch.clinicalSearchDropDowns.selectedHistologicalType - ); - const [patientJSON, setPatientJSON] = React.useState([]); - - // Dropdown patient table list for filtering - const [medicationList, setMedicationList] = React.useState(clinicalSearch.clinicalSearchDropDowns.medicationList); - const [conditionList, setConditionList] = React.useState(clinicalSearch.clinicalSearchDropDowns.conditionList); - const [sexList, setSexList] = React.useState(clinicalSearch.clinicalSearchDropDowns.sexList); - const [cancerTypeList, setCancerTypeList] = React.useState(clinicalSearch.clinicalSearchDropDowns.cancerTypeList); - const [HistologicalList, setHistologicalList] = React.useState(clinicalSearch.clinicalSearchDropDowns.HistologicalList); - - const jsonTheme = { - base00: 'white', - base01: '#ddd', - base02: '#ddd', - base03: 'black', - base04: '#0E3E17', - base05: 'black', - base06: 'black', - base07: '#252525', - base08: '#252525', - base09: '#00418A', - base0A: '#00418A', - base0B: '#00418A', - base0C: '#00418A', - base0D: '#00418A', - base0E: '#00418A', - base0F: '#00418A' - }; - - function setClincalSearchPatients(data) { - dispatch({ - type: 'SET_CLINICAL_SEARCH_PATIENTS', - payload: { - data - } - }); - } - - function setRedux(rows) { - const tempClinicalSearchResults = []; - rows.forEach((patient) => { - tempClinicalSearchResults.push({ id: patient.id, genomicId: patient.genomic_id }); - }); - dispatch({ - type: 'SET_SELECTED_CLINICAL_SEARCH_RESULTS', - payload: { - selectedClinicalSearchResults: tempClinicalSearchResults, - clinicalSearchDropDowns: { - medicationList, - selectedMedications, - conditionList, - selectedConditions, - sexList, - selectedSex, - cancerTypeList, - selectedCancerType, - HistologicalList, - selectedHistologicalType - } - } - }); - } - // Parsing CancerType CSV into Dictionary - React.useEffect( - () => - papa.parse(cancerTypeCSV, { - header: true, - download: true, - skipEmptyLines: true, - // eslint-disable-next-line - complete: function (results) { - setCancerType(results.data); - } - }), - [] - ); - // Subtable selection of patient - const handleRowClick = (row) => { - let index; - mcodeData.results.forEach((federatedResults) => { - index = federatedResults.results.findIndex((item) => item.id === row.id); - if (index !== -1) { - setSelectedPatient(federatedResults.results[index].id); - setSelectedPatientMobileInfo({ - Ethnicity: federatedResults?.results[index]?.subject?.ethnicity - ? federatedResults?.results[index]?.subject?.ethnicity - : 'NA', - Sex: (federatedResults?.results[index]?.subject?.sex).toLowerCase() - ? (federatedResults?.results[index]?.subject?.sex).toLowerCase() - : 'NA', - Deceased: federatedResults?.results[index]?.subject?.deceased - ? federatedResults?.results[index]?.subject?.deceased - : 'NA', - Birthday: federatedResults?.results[index]?.subject?.date_of_birth - ? federatedResults?.results[index]?.subject?.date_of_birth - : 'NA', - DeathDate: federatedResults?.results[index]?.date_of_death ? federatedResults?.results[index]?.date_of_death : 'NA' - }); - - // Set patient JSON - setPatientJSON(federatedResults?.results[index], selectedPatient); - } - }); - - setListOpen(false); - }; - - const dropDownSelection = (dropDownGroup, selected) => { - if (dropDownGroup === 'CONDITIONS') { - setSelectedConditions(selected); - setListOpenConditions(false); - } else if (dropDownGroup === 'MEDICATIONS') { - setSelectedMedications(selected); - setListOpenMedications(false); - } else if (dropDownGroup === 'SEX') { - setSelectedSex(selected); - setListOpenSex(false); - } else if (dropDownGroup === 'CANCER TYPE') { - setSelectedCancerType(selected); - setListOpenCancerType(false); - } else if (dropDownGroup === 'HISTOLOGICAL') { - setSelectedHistologicalType(selected); - setListOpenHistological(false); - } - }; - - // Filtering Data - React.useEffect(() => { - if (Object.keys(clinicalSearchPatients.data).length !== 0) { - const tempRows = []; - const data = clinicalSearchPatients.data; - for (let j = 0; j < data.results.length; j += 1) { - for (let i = 0; i < data.results[j].count; i += 1) { - // Patient table filtering - if ( - selectedConditions === 'All' && - selectedMedications === 'All' && - selectedSex === 'All' && - selectedCancerType === 'All' && - selectedHistologicalType === 'All' - ) { - // All patients - if (processMCodeMainData(data.results[j].results[i], data.results[j].location[0]).id !== null) { - tempRows.push(processMCodeMainData(data.results[j].results[i], data.results[j].location[0])); - } - } else { - // Filtered patients - let patientCondition = false; - data?.results[j]?.results[i]?.cancer_condition?.body_site?.every((bodySite) => { - if (selectedConditions === 'All' || selectedConditions === bodySite.label) { - patientCondition = true; - return false; - } - return true; - }); - let patientMedication = false; - data?.results[j]?.results[i]?.medication_statement.every((medication) => { - if (selectedMedications === 'All' || selectedMedications === medication?.medication_code.label) { - patientMedication = true; - return false; - } - return true; - }); - let patientSex = false; - if (selectedSex === 'All' || selectedSex === (data?.results[j]?.results[i]?.subject.sex).toLowerCase()) { - patientSex = true; - } - let patientCancerType = false; - if (selectedCancerType === 'All') { - patientCancerType = true; - } else { - for (let k = 0; k < cancerType.length; k += 1) { - if ( - data?.results[j]?.results[i]?.cancer_condition?.code?.id !== undefined && - data?.results[j]?.results[i]?.cancer_condition?.code?.id === cancerType[k]['Cancer type code'] - ) { - if ( - selectedCancerType === - `${cancerType[k]['Cancer type label']} ${cancerType[k]['Cancer type code']}` || - selectedCancerType === 'NA' - ) { - patientCancerType = true; - } - } - } - } - let patientHistologicalType = false; - if (selectedHistologicalType === 'All') { - patientHistologicalType = true; - } else { - for (let k = 0; k < cancerType.length; k += 1) { - if ( - data?.results[j]?.results[i]?.cancer_condition?.histology_morphology_behavior?.id !== undefined && - data?.results[j]?.results[i]?.cancer_condition?.histology_morphology_behavior?.id === - cancerType[k]['Tumour histological type code'] - ) { - if ( - selectedHistologicalType === - `${cancerType[k]['Tumour histological type label']} ${cancerType[k]['Tumour histological type code']}` || - selectedHistologicalType === 'NA' - ) { - patientHistologicalType = true; - } - } - } - } - if ( - patientCondition && - patientMedication && - patientSex && - patientCancerType && - patientHistologicalType && - processMCodeMainData(data.results[j].results[i]).id !== null - ) { - tempRows.push(processMCodeMainData(data.results[j].results[i], data.results[j].location[0])); - } - } - } - } - setRows(tempRows); - // Subtables - if (tempRows.length !== 0) { - let index; - data.results.forEach((federatedResults) => { - index = federatedResults.results.findIndex((item) => item.id === tempRows[0].id); - if (index !== -1) { - setSelectedPatient(federatedResults.results[index].id); - setPatientJSON(federatedResults.results[index], selectedPatient); - if (tempRows[0].id !== null) { - setSelectedPatientMobileInfo({ - Ethnicity: tempRows[0]?.ethnicity ? tempRows[0]?.ethnicity : 'NA', - Sex: tempRows[0]?.sex ? tempRows[0]?.sex : 'NA', - Deceased: tempRows[0]?.deceased ? tempRows[0]?.deceased : 'NA', - Birthday: tempRows[0]?.date_of_birth ? tempRows[0]?.date_of_birth : 'NA', - DeathDate: tempRows[0]?.date_of_death ? tempRows[0]?.date_of_death : 'NA' - }); - } - } - }); - } else { - setSelectedPatient('None'); - setPatientJSON({}); - } - - setListOpen(false); - // Dropdown patient table list for filtering - setMedicationList(processMedicationListData(data.results)); - setConditionList(processCondtionsListData(data.results)); - setSexList(processSexListData(data.results)); - setCancerTypeList(processCancerTypeListData(data.results)); - setHistologicalList(processHistologicalTypeListData(data.results)); - setIsLoading(false); - - setRedux(tempRows); - } - }, [selectedSex, selectedConditions, selectedMedications, selectedCancerType, selectedHistologicalType]); - - // Tracks Screensize - React.useEffect(() => { - window.addEventListener('resize', () => setdesktopResolution(window.innerWidth > 1200)); - }, [desktopResolution, setdesktopResolution]); - - React.useEffect(() => { - setIsLoading(true); - const tempRows = []; - trackPromise( - fetchFederationClinicalData().then((data) => { - setMcodeData(data); - setClincalSearchPatients(data); - for (let j = 0; j < data.results.length; j += 1) { - for (let i = 0; i < data.results[j].count; i += 1) { - // Patient table filtering - if ( - selectedConditions === 'All' && - selectedMedications === 'All' && - selectedSex === 'All' && - selectedCancerType === 'All' && - selectedHistologicalType === 'All' - ) { - // All patients - if (processMCodeMainData(data.results[j].results[i], data.results[j].location[0]).id !== null) { - tempRows.push(processMCodeMainData(data.results[j].results[i], data.results[j].location[0])); - } - } else { - // Filtered patients - let patientCondition = false; - data?.results[j]?.results[i]?.cancer_condition?.body_site?.every((bodySite) => { - if (selectedConditions === 'All' || selectedConditions === bodySite.label) { - patientCondition = true; - return false; - } - return true; - }); - let patientMedication = false; - data?.results[j]?.results[i]?.medication_statement.every((medication) => { - if (selectedMedications === 'All' || selectedMedications === medication?.medication_code.label) { - patientMedication = true; - return false; - } - return true; - }); - let patientSex = false; - if (selectedSex === 'All' || selectedSex === (data?.results[j]?.results[i]?.subject.sex).toLowerCase()) { - patientSex = true; - } - let patientCancerType = false; - if (selectedCancerType === 'All') { - patientCancerType = true; - } else { - for (let k = 0; k < cancerType.length; k += 1) { - if ( - data?.results[j]?.results[i]?.cancer_condition?.code?.id !== undefined && - data?.results[j]?.results[i]?.cancer_condition?.code?.id === cancerType[k]['Cancer type code'] - ) { - if ( - selectedCancerType === - `${cancerType[k]['Cancer type label']} ${cancerType[k]['Cancer type code']}` || - selectedCancerType === 'NA' - ) { - patientCancerType = true; - } - } - } - } - let patientHistologicalType = false; - if (selectedHistologicalType === 'All') { - patientHistologicalType = true; - } else { - for (let k = 0; k < cancerType.length; k += 1) { - if ( - data?.results[j]?.results[i]?.cancer_condition?.histology_morphology_behavior?.id !== undefined && - data?.results[j]?.results[i]?.cancer_condition?.histology_morphology_behavior?.id === - cancerType[k]['Tumour histological type code'] - ) { - if ( - selectedHistologicalType === - `${cancerType[k]['Tumour histological type label']} ${cancerType[k]['Tumour histological type code']}` || - selectedHistologicalType === 'NA' - ) { - patientHistologicalType = true; - } - } - } - } - if ( - patientCondition && - patientMedication && - patientSex && - patientCancerType && - patientHistologicalType && - processMCodeMainData(data.results[j].results[i]).id !== null - ) { - tempRows.push(processMCodeMainData(data.results[j].results[i], data.results[j].location[0])); - } - } - } - } - setRows(tempRows); - // Subtables - if (tempRows.length !== 0) { - let index; - data.results.forEach((federatedResults) => { - index = federatedResults.results.findIndex((item) => item.id === tempRows[0].id); - if (index !== -1) { - setSelectedPatient(federatedResults.results[index].id); - setPatientJSON(federatedResults.results[index], selectedPatient); - if (tempRows[0].id !== null) { - setSelectedPatientMobileInfo({ - Ethnicity: tempRows[0]?.ethnicity ? tempRows[0]?.ethnicity : 'NA', - Sex: tempRows[0]?.sex ? tempRows[0]?.sex : 'NA', - Deceased: tempRows[0]?.deceased ? tempRows[0]?.deceased : 'NA', - Birthday: tempRows[0]?.date_of_birth ? tempRows[0]?.date_of_birth : 'NA', - DeathDate: tempRows[0]?.date_of_death ? tempRows[0]?.date_of_death : 'NA' - }); - } - } - }); - } else { - setSelectedPatient('None'); - setPatientJSON({}); - } - - setListOpen(false); - // Dropdown patient table list for filtering - setMedicationList(processMedicationListData(data.results)); - setConditionList(processCondtionsListData(data.results)); - setSexList(processSexListData(data.results)); - setCancerTypeList(processCancerTypeListData(data.results)); - setHistologicalList(processHistologicalTypeListData(data.results)); - setIsLoading(false); - - setRedux(tempRows); - }), - 'table' - ); - }, []); - - // JSON on bottom now const screenWidth = desktopResolution ? '48%' : '100%'; - const headerLabels = { - Ethnicity: 'Ethnicity', - Sex: 'Sex', - Deceased: 'Deceased', - Birthday: 'Date of Birth', - DeathDate: 'Date of Death' - }; - const headerWidths = { - Ethnicity: '85px', - Sex: '85px', - Deceased: '85px', - Birthday: '100px', - DeathDate: '110px' - }; - - return ( - - - {selectedPatient && desktopResolution && ( - - - }> - - - - - - - - - )} - {!desktopResolution && selectedPatient && ( - - )} - - {!isLoading ? ( - - {desktopResolution && ( - - handleRowClick(rowData.row)} - className={classes.scrollbar} - disableSelectionOnClick - /> - - )} - - - - Patient Id - - - {selectedPatient} - - - - - - - ) : ( - - )} - - - ); -} - -export default MCodeView; diff --git a/src/views/clinicalGenomic/clinicalGenomicSearch.js b/src/views/clinicalGenomic/clinicalGenomicSearch.js index bda5ddb0..49334472 100644 --- a/src/views/clinicalGenomic/clinicalGenomicSearch.js +++ b/src/views/clinicalGenomic/clinicalGenomicSearch.js @@ -1,11 +1,10 @@ import { useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { AppBar, Button, Divider, Toolbar, Typography } from '@mui/material'; +import { AppBar, Button, Toolbar, Typography } from '@mui/material'; -import { makeStyles, useTheme } from '@mui/styles'; +import { makeStyles } from '@mui/styles'; import MainCard from 'ui-component/cards/MainCard'; -import VariantsSearch from '../genomicsData/VariantsSearch'; import PatientCounts from './widgets/patientCounts'; import DataVisualization from './widgets/dataVisualization'; import ClinicalData from './widgets/clinicalData'; @@ -16,7 +15,7 @@ import { COHORTS } from 'store/constant'; import SearchHandler from './search/SearchHandler'; import GenomicData from './widgets/genomicData'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles((_) => ({ stickytop: { position: 'fixed', backgroundColor: 'white', @@ -81,12 +80,11 @@ function ClinicalGenomicSearch() { const classes = useStyles(); const sidebarWriter = useSidebarWriterContext(); const sidebarOpened = useSelector((state) => state.customization.opened); - const theme = useTheme(); // When we load, set the sidebar component useEffect(() => { sidebarWriter(); - }, []); + }, [sidebarWriter]); return ( <> @@ -121,11 +119,6 @@ function ClinicalGenomicSearch() { - - {/* Genomic Searchbar */} - - {/* For now, until I figure out how to make it its own card */} - {sections.map((section) => ( diff --git a/src/views/clinicalGenomic/search/SearchHandler.js b/src/views/clinicalGenomic/search/SearchHandler.js index b120fb25..acb1a43c 100644 --- a/src/views/clinicalGenomic/search/SearchHandler.js +++ b/src/views/clinicalGenomic/search/SearchHandler.js @@ -1,13 +1,13 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { trackPromise } from 'react-promise-tracker'; import { useSearchResultsWriterContext, useSearchQueryReaderContext } from '../SearchResultsContext'; -import { fetchFederationStat, fetchFederation, searchVariant, searchVariantByGene } from 'store/api'; +import { fetchFederationStat, fetchFederation, query } from 'store/api'; // This will grab all of the results from a query, but continue to consume all "next" from the pagination until we are complete // This defeats the purpose of pagination, and is frowned upon, but... deadlines -function ConsumeAllPages(url, resultConsumer, service = 'katsu') { +/* function ConsumeAllPages(url, resultConsumer, service = 'katsu') { const parsedData = {}; const RecursiveQuery = (data, idx) => { let nextQuery = null; @@ -18,11 +18,11 @@ function ConsumeAllPages(url, resultConsumer, service = 'katsu') { parsedData[loc.location.name] = []; } - if (loc.results.next) { + if (loc.results?.next) { nextQuery = `${url}${url.includes('?') ? '&' : '?'}page=${idx + 1}`; } - if (loc.results.results) { + if (loc.results?.results) { parsedData[loc.location.name] = parsedData[loc.location.name].concat(loc.results.results.map(resultConsumer)); } }); @@ -35,12 +35,17 @@ function ConsumeAllPages(url, resultConsumer, service = 'katsu') { }; return fetchFederation(url, service).then((data) => RecursiveQuery(data, 1)); -} +} */ + +// NB: I assign to lastPromise a bunch to keep track of whether or not we need to chain promises together +// However, the linter really dislikes this, and assumes I want to put everything inside one useEffect? +/* eslint-disable react-hooks/exhaustive-deps */ // This handles transforming queries in the SearchResultsContext to actual search queries function SearchHandler() { const reader = useSearchQueryReaderContext(); const writer = useSearchResultsWriterContext(); + const [controller, _] = useState(new AbortController()); // Query 1: always have the federation sites and authorized programs query results available let lastPromise = null; @@ -69,267 +74,92 @@ function SearchHandler() { // Query 2: when the search query changes, re-query the server useEffect(() => { - const searchParams = new URLSearchParams(); - /* - // Parse out search parameters according to what they are: - if (reader.query) { - Object.keys(reader.query).forEach((key) => { - searchParams.append(key, Array.isArray(reader.query[key]) ? reader.query[key].join(',') : reader.query[key]); - }); - } */ - - // Queries now return lists of donors(??? Why is it like this?) so we need to AND/OR them as needed - if (reader.query) { - Object.keys(reader.query).forEach((key) => { - searchParams.append(key, Array.isArray(reader.query[key]) ? reader.query[key].join(',') : reader.query[key]); + // First, we abort any currently-running search promises + // controller.abort(); + console.log('Query re-initiated'); + console.log(reader.query); + + const CollateSummary = (data, statName) => { + const summaryStat = {}; + data.forEach((site) => { + const thisStat = site.results?.summary?.[statName]; + if (!thisStat) { + return; + } + + Object.keys(thisStat).forEach((key) => { + if (key in summaryStat) { + summaryStat[key] += thisStat[key]; + } else { + summaryStat[key] = thisStat[key]; + } + }); }); - } - - // From the donorLists, AND/OR them as needed until we have just one final list of acceptable donors - let finalList = null; - if (reader.donorLists && Object.keys(reader.donorLists).length > 0) { - const allLists = Object.keys(reader.donorLists)?.map((key) => - // Lists from the same queries are OR'd together - [...new Set(Object.values(reader?.donorLists[key])?.flat(1))] - ); - - // Lists from different queries are AND'd together - const toQuery = allLists[0]; - finalList = toQuery.filter((donor) => allLists.every((list) => list.includes(donor))); - - // TODO: Figure out pagination (again) - searchParams.append('donors', finalList.join(',')); - } - - searchParams.append('page_size', 100); - const url = `v2/authorized/donors?${searchParams}`; + return summaryStat; + }; const donorQueryPromise = () => - trackPromise( - ConsumeAllPages(url, (donor) => donor, 'katsu').then((data) => { - // We need to do two things: - // 1. Push the raw data into the context - // 2. Go through the data and fill out our pseudo-discovery queries for data vis - const discoveryCounts = { - diagnosis_age_count: {}, - treatment_type_count: {}, - cancer_type_count: {}, - patients_per_cohort: {}, - - // Below is test data - full_clinical_data: { - BCGSC: { - POG: 30 - }, - UHN: { - POG: 14, - Inspire: 20, - Biocan: 20, - Biodiva: 10 - }, - C3G: { - MOCK: 30 - } + query(reader.query, controller.signal).then((data) => { + if (reader.filter?.node) { + data = data.filter((site) => !reader.filter.node.includes(site.location.name)); + } + + // We need to collate the discovery statistics from each site + const discoveryCounts = { + diagnosis_age_count: CollateSummary(data, 'age_at_diagnosis'), + treatment_type_count: CollateSummary(data, 'treatment_type_count'), + cancer_type_count: CollateSummary(data, 'cancer_type_count'), + patients_per_cohort: {}, + + // Below is test data + full_clinical_data: { + BCGSC: { + POG: 30 }, - full_genomic_data: { - BCGSC: { - POG: 10 - }, - UHN: { - POG: 4, - Inspire: 10, - Biocan: 12, - Biodiva: 12 - }, - C3G: { - MOCK: 3 - } - } - }; - - // Add to a dictionary, or increment its value if it already exists - const addOrReplace = (dict, value) => { - if (value in dict) { - dict[value] += 1; - } else { - dict[value] = 1; - } - }; - - // Go through the donors we have, fill out our data visualization counts - const donorToDOB = {}; - Object.keys(data).forEach((locName) => { - if (reader.filter?.node?.includes(locName)) { - // Don't process filtered-out nodes - return; + UHN: { + POG: 14, + Inspire: 20, + Biocan: 20, + Biodiva: 10 + }, + C3G: { + MOCK: 30 } - - discoveryCounts.patients_per_cohort[locName] = {}; - donorToDOB[locName] = {}; - data[locName].forEach((donor) => { - if (reader.filter?.program_id?.includes(donor.program_id)) { - // Don't process filtered-out programs - return; - } - - if (donor.date_of_birth) { - donorToDOB[locName][donor.submitter_donor_id] = donor.date_of_birth; - } - donor.primary_site.forEach((site) => addOrReplace(discoveryCounts.cancer_type_count, site)); - addOrReplace(discoveryCounts.patients_per_cohort[locName], donor.program_id); - }); - }); - - // Finally, to finish out our counts, we need to query the primary diagnoses - return ConsumeAllPages( - 'v2/authorized/treatments/?page_size=100', - (treatment) => [treatment.submitter_donor_id, treatment.treatment_type], - 'katsu' - ) - .then((treatments) => { - Object.keys(treatments).forEach((locName) => { - if (reader.filter?.node?.includes(locName)) { - // Don't process filtered-out nodes - return; - } - - treatments[locName]?.forEach(([donorID, treatmentTypes]) => { - if (donorID in donorToDOB[locName]) { - treatmentTypes.forEach((treatmentType) => { - addOrReplace(discoveryCounts.treatment_type_count, treatmentType); - }); - } - }); - }); - }) - .then(() => - ConsumeAllPages( - 'v2/authorized/primary_diagnoses/?page_size=100', - (diagnosis) => [diagnosis.submitter_donor_id, diagnosis.date_of_diagnosis, diagnosis.program_id], - 'katsu' - ) - ) - .then((diagnoses) => { - Object.keys(diagnoses).forEach((locName) => { - if (reader.filter?.node?.includes(locName)) { - // Don't process filtered-out nodes - return; - } - - diagnoses[locName]?.forEach(([donorID, dateOfDiagnosis, programID]) => { - if (reader.filter?.program_id?.includes(programID)) { - // Don't process filtered-out programs - return; - } - - if (donorID in donorToDOB[locName] && dateOfDiagnosis) { - // Convert this to an age range - const diagDate = dateOfDiagnosis.split('-'); - const birthDate = donorToDOB[locName][donorID].split('-'); - let ageAtDiagnosis = diagDate[0] - birthDate[0] + (diagDate[1] >= birthDate[1] ? 1 : 0); - ageAtDiagnosis -= ageAtDiagnosis % 10; - - if (ageAtDiagnosis < 20) { - ageAtDiagnosis = '0-19 Years'; - } else if (ageAtDiagnosis > 79) { - ageAtDiagnosis = '80+ Years'; - } else { - ageAtDiagnosis = `${ageAtDiagnosis}-${ageAtDiagnosis + 9} Years`; - } - addOrReplace(discoveryCounts.diagnosis_age_count, ageAtDiagnosis); - - delete donorToDOB[locName][donorID]; - } - }); - }); - writer((old) => ({ ...old, clinical: data, counts: discoveryCounts })); - }); - }), - 'clinical' - ) - .then( - // Grab the specimens from the backend - () => - ConsumeAllPages( - 'v2/authorized/sample_registrations/?page_size=100', - (sample) => [sample.submitter_sample_id, sample.submitter_donor_id, sample.tumour_normal_designation], - 'katsu' - ) - ) - .then((data) => { - // First, we need to parse out all of the samples that exist inside of our finalList (if any) - const specimenToDonor = {}; - const specimenType = {}; - Object.keys(data).forEach((locName) => { - if (reader.filter?.node?.includes(locName)) { - // Don't process filtered-out nodes - return; + }, + full_genomic_data: { + BCGSC: { + POG: 10 + }, + UHN: { + POG: 4, + Inspire: 10, + Biocan: 12, + Biodiva: 12 + }, + C3G: { + MOCK: 3 } - - specimenToDonor[locName] = {}; - specimenType[locName] = {}; - data[locName]?.forEach(([sampleID, donorID, tumourNormalDesignation]) => { - specimenToDonor[locName][sampleID] = donorID; - specimenType[locName][sampleID] = tumourNormalDesignation; - }); - }); - - // We may need to query the HTSGet portion in order to do genomics search. - let htsgetPromise = null; - if (reader.genomic?.gene) { - htsgetPromise = searchVariantByGene(reader.genomic.gene); - } else if (reader.genomic?.referenceName) { - htsgetPromise = searchVariant( - reader.genomic?.referenceName, - reader.genomic?.start, - reader.genomic?.end, - reader.genomic?.assemblyId - ); } + }; - if (htsgetPromise) { - htsgetPromise.then((htsgetData) => { - // Parse out the response from Beacon - const htsgetFilteredData = htsgetData - .map((loc) => { - const handovers = loc.results?.beaconHandovers; - return ( - loc.results.response?.map((response) => - response.caseLevelData - .map((caseData) => { - // We need to map before filtering because we need to parse out the program & donor ID - const id = caseData.biosampleId.split('~'); - if (id.length > 1) { - caseData.program_id = id[0]; - caseData.submitter_specimen_id = id[1]; - caseData.donorID = - specimenToDonor[loc.location.name]?.[caseData.submitter_specimen_id]; - caseData.tumour_normal_designation = - specimenType[loc.location.name]?.[caseData.submitter_specimen_id]; - } else { - caseData.submitter_specimen_id = caseData.biosampleId; - } + // Reorder the data, and fill out the patients per cohort + const clinicalData = {}; + data.forEach((site) => { + discoveryCounts.patients_per_cohort[site.location.name] = site.results?.summary?.patients_per_cohort; + clinicalData[site.location.name] = site?.results?.results; + }); - caseData.beaconHandover = handovers[0]; - caseData.location = loc.location; - caseData.position = response.variation.location.interval.start.value; - return caseData; - }) - .filter((caseData) => { - if (reader.query && Object.keys(reader.query).length > 0 && caseData.donorID) { - return finalList.includes(caseData.donorID); - } - return true; - }) - ) || [] - ); - }) - .flat(2); + const genomicData = data + .map((site) => + site.results.genomic?.map((caseData) => { + caseData.location = site.location; + return caseData; + }) + ) + .flat(1); - writer((old) => ({ ...old, genomic: htsgetFilteredData })); - }); - } - }); + writer((old) => ({ ...old, clinical: clinicalData, counts: discoveryCounts, genomic: genomicData })); + }); if (lastPromise === null) { lastPromise = donorQueryPromise(); @@ -357,5 +187,6 @@ function SearchHandler() { // NB: This might be a good reason to have this be a function call instead of what it currently is. return <>>; } +/* eslint-enable react-hooks/exhaustive-deps */ export default SearchHandler; diff --git a/src/views/clinicalGenomic/widgets/clinicalData.js b/src/views/clinicalGenomic/widgets/clinicalData.js index 71cf02d9..977d6152 100644 --- a/src/views/clinicalGenomic/widgets/clinicalData.js +++ b/src/views/clinicalGenomic/widgets/clinicalData.js @@ -1,7 +1,7 @@ import * as React from 'react'; // mui -import { useTheme, makeStyles } from '@mui/styles'; +import { useTheme } from '@mui/styles'; import { DataGrid } from '@mui/x-data-grid'; import { Box, Typography } from '@mui/material'; @@ -10,38 +10,6 @@ import { Box, Typography } from '@mui/material'; // project imports import { useSearchQueryWriterContext, useSearchResultsReaderContext } from '../SearchResultsContext'; -// Styles -const useStyles = makeStyles({ - dropdownItem: { - background: 'white', - paddingRight: '1.25em', - paddingLeft: '1.25em', - border: 'none', - width: 'fit-content(5em)', - '&:hover': { - background: '#2196f3', - color: 'white' - } - }, - mobileRow: { - width: '800px' - }, - scrollbar: { - scrollbarWidth: 'thin', - '&::-webkit-scrollbar': { - height: '0.4em', - width: '0.4em' - }, - '&::-webkit-scrollbar-track': { - boxShadow: 'inset 0 0 4px rgba(0,0,0,0.00)', - webkitBoxShadow: 'inset 0 0 4px rgba(0,0,0,0.00)' - }, - '&::-webkit-scrollbar-thumb': { - backgroundColor: 'rgba(0,0,0,.1)' - } - } -}); - function ClinicalView() { const theme = useTheme(); @@ -77,11 +45,11 @@ function ClinicalView() { // JSON on bottom now const screenWidth = desktopResolution ? '48%' : '100%'; const columns = [ - { field: 'submitter_donor_id', headerName: 'Donor ID', minWidth: 220 }, - { field: 'sex_at_birth', headerName: 'Sex At Birth', minWidth: 170 }, - { field: 'deceased', headerName: 'Deceased', minWidth: 170 }, - { field: 'date_of_birth', headerName: 'Date of Birth', minWidth: 200 }, - { field: 'date_of_death', headerName: 'Date of Death', minWidth: 220 } + { field: 'submitter_donor_id', headerName: 'Donor ID', minWidth: 220, sortable: false }, + { field: 'sex_at_birth', headerName: 'Sex At Birth', minWidth: 170, sortable: false }, + { field: 'deceased', headerName: 'Deceased', minWidth: 170, sortable: false }, + { field: 'date_of_birth', headerName: 'Date of Birth', minWidth: 200, sortable: false }, + { field: 'date_of_death', headerName: 'Date of Death', minWidth: 220, sortable: false } ]; return ( diff --git a/src/views/clinicalGenomic/widgets/dataVisualization.js b/src/views/clinicalGenomic/widgets/dataVisualization.js index b1bd6cea..c04e7d4a 100644 --- a/src/views/clinicalGenomic/widgets/dataVisualization.js +++ b/src/views/clinicalGenomic/widgets/dataVisualization.js @@ -11,7 +11,6 @@ import DialogActions from '@mui/material/DialogActions'; import Button from '@mui/material/Button'; // Third-party libraries -import Cookies from 'js-cookie'; import { IconEdit, IconX, IconPlus } from '@tabler/icons'; // Custom Components and context @@ -21,7 +20,7 @@ import { useSearchResultsReaderContext } from '../SearchResultsContext'; // Constants import { validStackedCharts, DataVisualizationChartInfo } from 'store/constant'; -function DataVisualization(props) { +function DataVisualization() { // Hooks const resultsContext = useSearchResultsReaderContext().counts; // Plan for context below see current dataVis for expected shape @@ -69,28 +68,32 @@ function DataVisualization(props) { // Top 4 keys from dataVis const topKeys = Object.keys(dataVis).slice(0, 4); - // Cookies + // LocalStorage const [dataValue, setDataValue] = useState( - Cookies.get('dataVisData') ? JSON.parse(Cookies.get('dataVisData'))[0] : 'patients_per_cohort' + localStorage.getItem('dataVisData') ? JSON.parse(localStorage.getItem('dataVisData'))[0] : 'patients_per_cohort' + ); + const [chartType, setChartType] = useState( + localStorage.getItem('dataVisChartType') ? JSON.parse(localStorage.getItem('dataVisChartType'))[0] : 'bar' + ); + const [dataVisData, setdataVisData] = useState( + localStorage.getItem('dataVisData') ? JSON.parse(localStorage.getItem('dataVisData')) : topKeys ); - const [chartType, setChartType] = useState(Cookies.get('dataVisChartType') ? JSON.parse(Cookies.get('dataVisChartType'))[0] : 'bar'); - const [dataVisData, setdataVisData] = useState(Cookies.get('dataVisData') ? JSON.parse(Cookies.get('dataVisData')) : topKeys); const [dataVisChartType, setDataVisChartType] = useState( - Cookies.get('dataVisChartType') ? JSON.parse(Cookies.get('dataVisChartType')) : ['bar', 'line', 'column', 'bar'] + localStorage.getItem('dataVisChartType') ? JSON.parse(localStorage.getItem('dataVisChartType')) : ['bar', 'line', 'column', 'bar'] ); const [dataVisTrim, setDataVisTrim] = useState( - Cookies.get('dataVisTrim') ? JSON.parse(Cookies.get('dataVisTrim')) : [false, false, false, false] + localStorage.getItem('dataVisTrim') ? JSON.parse(localStorage.getItem('dataVisTrim')) : [false, false, false, false] ); - // Intial cookie setting if there are none + // Intial localStorage setting if there are none useEffect(() => { - if (!Cookies.get('dataVisData') && !Cookies.get('dataVisChartType')) { + if (!localStorage.getItem('dataVisData') && !localStorage.getItem('dataVisChartType')) { const charts = topKeys.map(() => 'bar'); - Cookies.set('dataVisChartType', JSON.stringify(charts), { expires: 365 }); - Cookies.set('dataVisData', JSON.stringify(topKeys), { expires: 365 }); - Cookies.set('dataVisTrim', JSON.stringify([false, false, false, false]), { expires: 365 }); + localStorage.setItem('dataVisChartType', JSON.stringify(charts), { expires: 365 }); + localStorage.setItem('dataVisData', JSON.stringify(topKeys), { expires: 365 }); + localStorage.setItem('dataVisTrim', JSON.stringify([false, false, false, false]), { expires: 365 }); } - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const handleToggleDialog = () => { setOpen((prevOpen) => !prevOpen); @@ -103,9 +106,9 @@ function DataVisualization(props) { setDataVisChartType(newDataVisChartType); setdataVisData(newdataVisData); setDataVisTrim(newDataVisTrim); - Cookies.set('dataVisData', JSON.stringify(newdataVisData), { expires: 365 }); - Cookies.set('dataVisChartType', JSON.stringify(newDataVisChartType), { expires: 365 }); - Cookies.set('dataVisTrim', JSON.stringify(newDataVisTrim), { expires: 365 }); + localStorage.setItem('dataVisData', JSON.stringify(newdataVisData), { expires: 365 }); + localStorage.setItem('dataVisChartType', JSON.stringify(newDataVisChartType), { expires: 365 }); + localStorage.setItem('dataVisTrim', JSON.stringify(newDataVisTrim), { expires: 365 }); } function AddChart(data, chartType) { @@ -116,9 +119,9 @@ function DataVisualization(props) { setDataVisChartType(newDataVisChartType); setdataVisData(newdataVisData); setDataVisTrim(newDataVisTrim); - Cookies.set('dataVisData', JSON.stringify(newdataVisData), { expires: 365 }); - Cookies.set('dataVisChartType', JSON.stringify(newDataVisChartType), { expires: 365 }); - Cookies.set('dataVisTrim', JSON.stringify(newDataVisTrim), { expires: 365 }); + localStorage.setItem('dataVisChartType', JSON.stringify(newDataVisChartType), { expires: 365 }); + localStorage.setItem('dataVisTrim', JSON.stringify(newDataVisTrim), { expires: 365 }); + localStorage.setItem('dataVisData', JSON.stringify(newdataVisData), { expires: 365 }); } /* eslint-disable jsx-a11y/no-onchange */ function returnChartDialog() { diff --git a/src/views/clinicalGenomic/widgets/genomicData.js b/src/views/clinicalGenomic/widgets/genomicData.js index d0ad1310..c8df639f 100644 --- a/src/views/clinicalGenomic/widgets/genomicData.js +++ b/src/views/clinicalGenomic/widgets/genomicData.js @@ -1,7 +1,7 @@ import * as React from 'react'; // mui -import { useTheme, makeStyles } from '@mui/styles'; +import { useTheme } from '@mui/styles'; import { DataGrid } from '@mui/x-data-grid'; import { Box, Typography } from '@mui/material'; @@ -10,38 +10,6 @@ import { Box, Typography } from '@mui/material'; // project imports import { useSearchQueryReaderContext, useSearchResultsReaderContext } from '../SearchResultsContext'; -// Styles -const useStyles = makeStyles({ - dropdownItem: { - background: 'white', - paddingRight: '1.25em', - paddingLeft: '1.25em', - border: 'none', - width: 'fit-content(5em)', - '&:hover': { - background: '#2196f3', - color: 'white' - } - }, - mobileRow: { - width: '800px' - }, - scrollbar: { - scrollbarWidth: 'thin', - '&::-webkit-scrollbar': { - height: '0.4em', - width: '0.4em' - }, - '&::-webkit-scrollbar-track': { - boxShadow: 'inset 0 0 4px rgba(0,0,0,0.00)', - webkitBoxShadow: 'inset 0 0 4px rgba(0,0,0,0.00)' - }, - '&::-webkit-scrollbar-thumb': { - backgroundColor: 'rgba(0,0,0,.1)' - } - } -}); - function GenomicData() { const theme = useTheme(); @@ -49,7 +17,7 @@ function GenomicData() { const [desktopResolution, setdesktopResolution] = React.useState(window.innerWidth > 1200); const searchResults = useSearchResultsReaderContext().genomic; - const query = useSearchQueryReaderContext().genomic; + const query = useSearchQueryReaderContext().query; // Flatten the search results so that we are filling in the rows let rows = []; @@ -79,17 +47,17 @@ function GenomicData() { // JSON on bottom now const screenWidth = desktopResolution ? '48%' : '100%'; const columns = [ - { field: 'location', headerName: 'Node', minWidth: 150 }, - { field: 'donorID', headerName: 'Donor ID', minWidth: 150 }, - { field: 'position', headerName: 'Position', minWidth: 200 }, - { field: 'tumour_normal_designation', headerName: 'Tumour/Normal', minWidth: 200 }, - { field: 'submitter_specimen_id', headerName: 'Tumour Specimen ID', minWidth: 200 }, - { field: 'genotypeLabel', headerName: 'Genotype', minWidth: 300 }, - { field: 'zygosityLabel', headerName: 'Zygosity', minWidth: 200 } + { field: 'location', headerName: 'Node', minWidth: 120, sortable: false }, + { field: 'donor_id', headerName: 'Donor ID', minWidth: 150, sortable: false }, + { field: 'position', headerName: 'Position', minWidth: 150, sortable: false }, + { field: 'tumour_normal_designation', headerName: 'Tumour/Normal', minWidth: 200, sortable: false }, + { field: 'submitter_specimen_id', headerName: 'Tumour Specimen ID', minWidth: 300, sortable: false }, + { field: 'genotypeLabel', headerName: 'Genotype', minWidth: 300, sortable: false }, + { field: 'zygosityLabel', headerName: 'Zygosity', minWidth: 200, sortable: false } ]; - const queryParams = query?.gene || `${query?.referenceName}:${query?.start}-${query?.end}`; - const hasValidQuery = (query?.referenceName && query?.start && query?.end) || query?.gene; + const queryParams = query?.gene || query?.chrom; + const hasValidQuery = (query?.assembly && query?.chrom) || query?.gene; return ( diff --git a/src/views/clinicalGenomic/widgets/patientCountSingle.js b/src/views/clinicalGenomic/widgets/patientCountSingle.js index b5a420ce..ecb2e4ba 100644 --- a/src/views/clinicalGenomic/widgets/patientCountSingle.js +++ b/src/views/clinicalGenomic/widgets/patientCountSingle.js @@ -1,9 +1,10 @@ -import { Fragment, useEffect, useState } from 'react'; +import { useState } from 'react'; import { Avatar, Box, Button, CardHeader, Divider, Grid, Typography } from '@mui/material'; import { useTheme, makeStyles } from '@mui/styles'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import UnfoldLessIcon from '@mui/icons-material/UnfoldLess'; +import PropTypes from 'prop-types'; const useStyles = makeStyles((theme) => ({ patientEntry: { @@ -138,4 +139,9 @@ function PatientCountSingle(props) { ); } +PatientCountSingle.propTypes = { + site: PropTypes.string, + counts: PropTypes.object +}; + export default PatientCountSingle; diff --git a/src/views/clinicalGenomic/widgets/patientCounts.js b/src/views/clinicalGenomic/widgets/patientCounts.js index 2f2481d9..4af074a8 100644 --- a/src/views/clinicalGenomic/widgets/patientCounts.js +++ b/src/views/clinicalGenomic/widgets/patientCounts.js @@ -14,7 +14,7 @@ const useStyles = makeStyles((theme) => ({ } })); -function PatientCounts(props) { +function PatientCounts() { const classes = useStyles(); const context = useSearchResultsReaderContext(); const sites = context?.federation; diff --git a/src/views/clinicalGenomic/widgets/patientView.js b/src/views/clinicalGenomic/widgets/patientView.js index 6cdb7d91..91b8f950 100644 --- a/src/views/clinicalGenomic/widgets/patientView.js +++ b/src/views/clinicalGenomic/widgets/patientView.js @@ -5,10 +5,11 @@ import { makeStyles, useTheme } from '@mui/styles'; import { TreeView, TreeItem } from '@mui/lab'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import PropTypes from 'prop-types'; import { useSearchResultsReaderContext } from '../SearchResultsContext'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles((_theme) => ({ label: { textTransform: 'capitalize', display: 'inline-flex' @@ -96,7 +97,49 @@ const JSONTree = (props) => { ); }; -function PatientView(props) { +JSONTree.propTypes = { + id: PropTypes.string, + label: PropTypes.string, + json: PropTypes.object, + searchExp: PropTypes.string +}; + +// Prune the patient entry according to search +const recursivePrune = (json, searchTerm) => { + if (Array.isArray(json)) { + // Create a new array with pruned children + const retVal = json.map((child) => recursivePrune(child, searchTerm)).filter((child) => child !== undefined); + return retVal.length <= 0 ? undefined : retVal; + } + + if (isObject(json)) { + // Find any key that matches the search + const retVal = {}; + Object.keys(json).forEach((key) => { + if (key.toLowerCase().indexOf(searchTerm) >= 0) { + // include all children of a matching parent + retVal[key] = json[key]; + return; + } + + // For all non-valid keys, there might be a valid child -- recurse downwards and prune + const childObj = recursivePrune(json[key], searchTerm); + if (childObj !== undefined) { + retVal[key] = childObj; + } + }); + return Object.keys(retVal).length > 0 ? retVal : undefined; + } + + if (typeof json === 'string' && json.toLowerCase().indexOf(searchTerm) >= 0) { + // Check if we're a match of the search + return json; + } + + return undefined; +}; + +function PatientView() { const resultsContext = useSearchResultsReaderContext(); const patient = resultsContext.donor?.map((loc) => loc?.results?.results?.[0])?.filter((donor) => donor)?.[0]; const [expanded, setExpanded] = useState(['.']); @@ -116,41 +159,6 @@ function PatientView(props) { return includeValues ? [`${prefix}:${typeof json === 'string' ? json.toLowerCase() : json}`] : []; }; - // Prune the patient entry according to search - const recursivePrune = (json, searchTerm) => { - if (Array.isArray(json)) { - // Create a new array with pruned children - const retVal = json.map((child) => recursivePrune(child, searchTerm)).filter((child) => child !== undefined); - return retVal.length <= 0 ? undefined : retVal; - } - - if (isObject(json)) { - // Find any key that matches the search - const retVal = {}; - Object.keys(json).forEach((key) => { - if (key.toLowerCase().indexOf(searchTerm) >= 0) { - // include all children of a matching parent - retVal[key] = json[key]; - return; - } - - // For all non-valid keys, there might be a valid child -- recurse downwards and prune - const childObj = recursivePrune(json[key], searchTerm); - if (childObj !== undefined) { - retVal[key] = childObj; - } - }); - return Object.keys(retVal).length > 0 ? retVal : undefined; - } - - if (typeof json === 'string' && json.toLowerCase().indexOf(searchTerm) >= 0) { - // Check if we're a match of the search - return json; - } - - return undefined; - }; - const handleExpandAll = () => { setExpanded((old) => (old.length <= 1 ? getAllChildIDs(prunedPatient, '.', false) : ['.'])); }; @@ -199,6 +207,7 @@ function PatientView(props) { }; // When the patient changes, we need to update the pruned view according to search + const patientString = JSON.stringify(patient); useEffect(() => { if (search !== '') { setPrunedPatient(recursivePrune(patient, search)); @@ -206,7 +215,7 @@ function PatientView(props) { // Otherwise, just reset the prunedPatient view to the patient view setPrunedPatient(patient); } - }, [JSON.stringify(patient)]); + }, [patientString]); // eslint-disable-line react-hooks/exhaustive-deps const noResultsMessage = !patient ? ( 'Please select a patient from the above table' diff --git a/src/views/clinicalGenomic/widgets/sidebar.js b/src/views/clinicalGenomic/widgets/sidebar.js index 2f5544fd..6ac5a076 100644 --- a/src/views/clinicalGenomic/widgets/sidebar.js +++ b/src/views/clinicalGenomic/widgets/sidebar.js @@ -15,11 +15,11 @@ import { import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; import { makeStyles, useTheme } from '@mui/styles'; +import PropTypes from 'prop-types'; import { useSearchQueryWriterContext, useSearchResultsReaderContext } from '../SearchResultsContext'; -import { fetchFederation } from '../../../store/api'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles((_) => ({ tab: { minWidth: 40 }, @@ -73,8 +73,14 @@ function SidebarGroup(props) { ); } +SidebarGroup.propTypes = { + name: PropTypes.string, + children: PropTypes.node, + hide: PropTypes.bool +}; + function StyledCheckboxList(props) { - const { groupName, isDonorList, isFilterList, remap, onWrite, options, useAutoComplete, hide } = props; + const { isExclusion, groupName, isFilterList, onWrite, options, useAutoComplete, hide } = props; const [checked, setChecked] = useState({}); const [initialized, setInitialized] = useState(false); const classes = useStyles(); @@ -96,76 +102,48 @@ function StyledCheckboxList(props) { const icon = ; const checkedIcon = ; - const HandleChange = (option, isChecked) => { - // If we need to call some mapping function, do so - let getID = new Promise((resolve) => resolve(option)); - if (remap) { - getID = remap(option); + const HandleChange = (ids, isChecked) => { + // Remove duplicates + if (Array.isArray(ids)) { + ids = Array.from(new Set(ids?.flat(1))); } - getID.then((ids) => { - // Remove duplicates - if (Array.isArray(ids)) { - ids = Array.from(new Set(ids?.flat(1))); - } + if (isExclusion ? !isChecked : isChecked) { + setChecked((old) => ({ ...old, [ids]: true })); + onWrite((old) => { + const retVal = { donorLists: {}, filter: {}, query: {}, ...old }; + + // The following appends ourselves to the write context under 'query': {group: [list]} or 'donorList': {group: [list]} + if (isFilterList) { + // Filter lists operate differently: you _remove_ the option when you check it + retVal.filter[groupName].splice(retVal.filter[groupName].indexOf(ids)); + } else { + retVal.query[groupName] = ids; + } - if (isChecked) { - setChecked((old) => ({ ...old, [option]: true })); - onWrite((old) => { - const retVal = { donorLists: {}, filter: {}, query: {}, ...old }; - - // The following appends ourselves to the write context under 'query': {group: [list]} or 'donorList': {group: [list]} - if (isDonorList) { - if (!(groupName in retVal.donorLists)) { - retVal.donorLists[groupName] = { [option]: ids }; - } else { - retVal.donorLists[groupName][option] = ids; - } - } else if (isFilterList) { - // Filter lists operate differently: you _remove_ the option when you check it - retVal.filter[groupName].splice(retVal.filter[groupName].indexOf(option)); + return retVal; + }); + } else { + setChecked((old) => { + const { [ids]: _, ...rest } = old; + return rest; + }); + onWrite((old) => { + const retVal = { filter: {}, ...old }; + if (isFilterList) { + if (groupName in retVal.filter) { + retVal.filter[groupName].push(ids); } else { - retVal.query[groupName] = ids; + retVal.filter[groupName] = [ids]; } - return retVal; - }); - } else { - setChecked((old) => { - const { [option]: _, ...rest } = old; - return rest; - }); - onWrite((old) => { - const retVal = { filter: {}, ...old }; - if (isFilterList) { - if (groupName in retVal.filter) { - retVal.filter[groupName].push(ids); - } else { - retVal.filter[groupName] = [ids]; - } - return retVal; - } - if (!isDonorList) { - const newList = Object.fromEntries(Object.entries(old.query).filter(([name, _]) => name !== groupName)); - retVal.query = newList; - return retVal; - } - - const newList = Object.entries(old.donorLists[groupName] || {}).filter(([key]) => key !== option); - - // Remove the list entirely if we are the last one - if (newList.length <= 0) { - const { [groupName]: _, ...rest } = old.donorLists || {}; - retVal.donorLists = rest; - return retVal; - } + } - // Otherwise remove just our entry from the list - retVal.donorLists[groupName] = Object.fromEntries(newList); - return retVal; - }); - } - }); + const newList = Object.fromEntries(Object.entries(old.query).filter(([name, _]) => name !== groupName)); + retVal.query = newList; + return retVal; + }); + } }; return useAutoComplete ? ( @@ -177,7 +155,13 @@ function StyledCheckboxList(props) { disableCloseOnSelect renderOption={(props, option, { selected }) => ( - + {option} )} @@ -195,7 +179,7 @@ function StyledCheckboxList(props) { control={ HandleChange(option, event.target.checked)} /> } @@ -205,37 +189,65 @@ function StyledCheckboxList(props) { )) ); } + +StyledCheckboxList.propTypes = { + isExclusion: PropTypes.bool, + groupName: PropTypes.string, + hide: PropTypes.bool, + isDonorList: PropTypes.bool, + isFilterList: PropTypes.bool, + remap: PropTypes.func, + onWrite: PropTypes.func, + options: PropTypes.array, + useAutoComplete: PropTypes.bool +}; + // A group of genomics data // Keeping this separate from the rest as it's all somewhat self-contained // NB: Should maybe go into a separate .js file function GenomicsGroup(props) { const { chromosomes, genes, onWrite, hide } = props; - const classes = useStyles(); // Genomic data - const referenceGenomes = ['hg38']; - const [selectedGenome, setSelectedGenome] = useState('hg38'); + // const referenceGenomes = ['hg38']; + const [selectedGenome, _setSelectedGenome] = useState('hg38'); const [selectedChromosomes, setSelectedChromosomes] = useState(''); const [selectedGenes, setSelectedGenes] = useState(''); const [startPos, setStartPos] = useState(0); const [endPos, setEndPos] = useState(0); + const [_timeout, setNewTimeout] = useState(null); if (hide) { return <>>; } const HandleChange = (value, changer, toChange) => { - changer(value); - onWrite((old) => ({ - ...old, - genomic: { - assemblyId: selectedGenome, - referenceName: selectedChromosomes, - start: startPos, - end: endPos, - gene: selectedGenes, - [toChange]: value + setNewTimeout((oldTimeout) => { + if (oldTimeout != null) { + clearTimeout(oldTimeout); } - })); + + return setTimeout(() => { + const newQuery = { + referenceName: selectedChromosomes, + gene: selectedGenes, + start: startPos, + end: endPos, + assembly: selectedGenome, + [toChange]: value + }; + + onWrite((old) => ({ + ...old, + query: { + ...old.query, + chrom: newQuery.referenceName ? `chr${newQuery.referenceName}:${newQuery.start}-${newQuery.end}` : undefined, + gene: newQuery.gene || undefined, + assembly: newQuery.assembly + } + })); + }, 1000); + }); + changer(value); }; return ( @@ -300,7 +312,14 @@ function GenomicsGroup(props) { ); } -function Sidebar(props) { +GenomicsGroup.propTypes = { + chromosomes: PropTypes.array, + genes: PropTypes.array, + hide: PropTypes.bool, + onWrite: PropTypes.func +}; + +function Sidebar() { const [selectedtab, setSelectedTab] = useState('All'); const readerContext = useSearchResultsReaderContext(); const writerContext = useSearchQueryWriterContext(); @@ -337,11 +356,6 @@ function Sidebar(props) { const hideGenomic = selectedtab !== 'All' && selectedtab !== 'Genomic'; const hideClinical = selectedtab !== 'All' && selectedtab !== 'Clinical'; - const remap = (url, returnName) => - fetchFederation(url, 'katsu').then( - (data) => data?.map((loc) => loc?.results?.results?.map((result) => result[returnName]) || []) || [] - ); - return ( <> setSelectedTab(value)}> @@ -353,7 +367,7 @@ function Sidebar(props) { - + @@ -361,8 +375,6 @@ function Sidebar(props) { options={treatmentTypes} onWrite={writerContext} groupName="treatment" - remap={(id) => remap(`v2/authorized/treatments?treatment_type=${id}`, 'submitter_donor_id')} - isDonorList useAutoComplete={treatmentTypes.length >= 10} hide={hideClinical} /> @@ -381,8 +393,6 @@ function Sidebar(props) { options={chemotherapyDrugNames} onWrite={writerContext} groupName="chemotherapy" - remap={(id) => remap(`v2/authorized/chemotherapies?drug_name=${id}`, 'submitter_donor_id')} - isDonorList useAutoComplete={chemotherapyDrugNames.length >= 10} hide={hideClinical} /> @@ -392,8 +402,6 @@ function Sidebar(props) { options={immunotherapyDrugNames} onWrite={writerContext} groupName="immunotherapy" - remap={(id) => remap(`v2/authorized/immunotherapies?drug_name=${id}`, 'submitter_donor_id')} - isDonorList useAutoComplete={immunotherapyDrugNames.length >= 10} hide={hideClinical} /> @@ -403,8 +411,6 @@ function Sidebar(props) { options={hormoneTherapyDrugNames} onWrite={writerContext} groupName="hormone_therapy" - remap={(id) => remap(`v2/authorized/hormone_therapies?drug_name=${id}`, 'submitter_donor_id')} - isDonorList useAutoComplete={hormoneTherapyDrugNames.length >= 10} hide={hideClinical} /> diff --git a/src/views/dashboard/Default/TotalIncomeDarkCard.js b/src/views/dashboard/Default/TotalIncomeDarkCard.js index c8a6abf4..62e0a2a4 100644 --- a/src/views/dashboard/Default/TotalIncomeDarkCard.js +++ b/src/views/dashboard/Default/TotalIncomeDarkCard.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import React from 'react'; // mui import { makeStyles } from '@mui/styles'; diff --git a/src/views/dashboard/Default/datasetIdSelect.js b/src/views/dashboard/Default/datasetIdSelect.js deleted file mode 100644 index fac49806..00000000 --- a/src/views/dashboard/Default/datasetIdSelect.js +++ /dev/null @@ -1,112 +0,0 @@ -import { useState, useEffect } from 'react'; -import Box from '@mui/material/Box'; -import MenuItem from '@mui/material/MenuItem'; -import FormControl from '@mui/material/FormControl'; -import Select from '@mui/material/Select'; -import { fetchDatasets } from '../../../store/api'; -import AlertComponent from 'ui-component/AlertComponent'; - -// REDUX -import { useSelector, useDispatch } from 'react-redux'; - -export default function DatasetIdSelect() { - const events = useSelector((state) => state); - const dispatch = useDispatch(); - - // STATES - const [selectedDataset, setSelectedDataset] = useState(events.customization.selectedDataset); - const [datasets, setDatasets] = useState(events.customization.datasets); - const [open, setOpen] = useState(false); - const [alertMessage, setAlertMessage] = useState(''); - const [alertSeverity, setAlertSeverity] = useState(''); - - function updateParentState(datasetName, datasetId) { - dispatch({ type: 'SET_UPDATE_STATE', payload: { datasetName, datasetId } }); - } - - function setFirstDataset(datasetsList) { - const firstDataset = datasetsList[Object.keys(datasetsList)[0]]; - setSelectedDataset(firstDataset.name); - updateParentState(firstDataset.name, firstDataset.id); - dispatch({ type: 'SET_SELECTED_DATASET', payload: firstDataset.name }); - } - - function processDatasetJson(datasetJson) { - const datasetsList = {}; - datasetJson.forEach((dataset) => { - datasetsList[dataset.id] = dataset; - }); - return datasetsList; - } - - useEffect(() => { - if (!selectedDataset) { - fetchDatasets() - .then((data) => { - if (data.results) { - const datasetsList = processDatasetJson(data.results.datasets); - setDatasets(datasetsList); - setFirstDataset(datasetsList); - dispatch({ type: 'SET_DATASETS', payload: datasetsList }); - } - }) - .catch(() => { - setOpen(true); - setAlertMessage('No datasets are currently available. If this issue persists, contact your sysadmin for assistance'); - setAlertSeverity('error'); - }); - } - }); - - const handleChange = (id, name) => { - setSelectedDataset(name); - updateParentState(name, id); // Suppose tp be id - dispatch({ type: 'SET_SELECTED_DATASET', payload: name }); - }; - - const datasetList = Object.keys(datasets).map((key) => ( - handleChange(datasets[key].id, datasets[key].name)}> - {datasets[key].name} - - )); - - return ( - - - {selectedDataset && ( - - {/* Dataset ID */} - - {datasetList} - - - )} - - - - ); -} diff --git a/src/views/dashboard/Default/index.js b/src/views/dashboard/Default/index.js index c40d70f2..56e12752 100644 --- a/src/views/dashboard/Default/index.js +++ b/src/views/dashboard/Default/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; // mui import { Grid } from '@mui/material'; diff --git a/src/views/genomicsData/HtsgetBrowser.js b/src/views/genomicsData/HtsgetBrowser.js index 703f9fd6..2728ab63 100644 --- a/src/views/genomicsData/HtsgetBrowser.js +++ b/src/views/genomicsData/HtsgetBrowser.js @@ -28,8 +28,8 @@ function HtsgetBrowser() { const [open, setOpen] = useState(false); const [selectedBamIds, setSelectedBamIds] = useState([]); const [selectedVcfIds, setSelectedVcfIds] = useState([]); - const [alertMessage, setAlertMessage] = useState(''); - const [alertSeverity, setAlertSeverity] = useState(''); + const [alertMessage, _setAlertMessage] = useState(''); + const [alertSeverity, _setAlertSeverity] = useState(''); useEffect(() => { setSelected([]); @@ -41,7 +41,7 @@ function HtsgetBrowser() { // Get a list of genomics files available trackPromise( fetchKatsu('/api/genomicsreports') - .then((data) => { + .then((_data) => { // TODO: get a list of ids of genomics files from API above // Or any other APIs that can provide a list of IDs // Note: if only VCF files are served, then the following TODOs concerning BAM files diff --git a/src/views/genomicsData/IGViewer.js b/src/views/genomicsData/IGViewer.js index 08b7a287..ecfa29f2 100644 --- a/src/views/genomicsData/IGViewer.js +++ b/src/views/genomicsData/IGViewer.js @@ -1,24 +1,36 @@ +import { useState } from 'react'; + +import { Alert, Snackbar } from '@mui/material'; + import NewWindow from 'react-new-window'; import CramVcfInstance from 'ui-component/IGV/CramVcfInstance'; import PropTypes from 'prop-types'; const IGViewer = ({ closeWindow, options }) => { + const [open, setOpen] = useState(false); const onClosed = () => { closeWindow(); }; return ( - alert('Please allow popups for this website')} - features={{ - outerHeight: '100%', - outerWidth: '100%' - }} - > - - + <> + setOpen(false)}> + setOpen(false)} severity="error" sx={{ width: '100%' }}> + Please allow popups for this website + + + setOpen(true)} + features={{ + outerHeight: '100%', + outerWidth: '100%' + }} + > + + + > ); }; diff --git a/src/views/genomicsData/VariantsSearch.js b/src/views/genomicsData/VariantsSearch.js deleted file mode 100644 index fb037e46..00000000 --- a/src/views/genomicsData/VariantsSearch.js +++ /dev/null @@ -1,571 +0,0 @@ -import * as React from 'react'; - -import { useState, useEffect } from 'react'; -import { Grid, Button, FormControl, InputLabel, Input, NativeSelect, Box } from '@mui/material'; -import { useSelector, useDispatch } from 'react-redux'; -import VariantsTable from 'ui-component/Tables/VariantsTable'; -import { SearchIndicator } from 'ui-component/LoadingIndicator/SearchIndicator'; -import AlertComponent from 'ui-component/AlertComponent'; -import { ListOfReferenceNames } from 'store/constant'; -import { trackPromise, usePromiseTracker } from 'ui-component/LoadingIndicator/LoadingIndicator'; -import 'assets/css/VariantsSearch.css'; -import { searchVariant, fetchFederationClinicalData, htsget } from 'store/api'; -import IGViewer from './IGViewer'; -import DropDown from '../../ui-component/DropDown'; -import { - processMCodeMainData, - processMedicationListData, - processCondtionsListData, - processSexListData, - processCancerTypeListData, - processHistologicalTypeListData -} from 'store/mcode'; - -// mui -import { useTheme } from '@mui/styles'; -import Stack from '@mui/material/Stack'; -import Divider from '@mui/material/Divider'; - -function VariantsSearch() { - const [isLoading, setLoading] = useState(true); - const theme = useTheme(); - const dispatch = useDispatch(); - const events = useSelector((state) => state); - - const [rowData, setRowData] = useState([]); - const [displayVariantsTable, setDisplayVariantsTable] = useState(false); - const { promiseInProgress } = usePromiseTracker(); - const [open, setOpen] = useState(false); - const [alertMessage, setAlertMessage] = useState(''); - const [alertSeverity, setAlertSeverity] = useState('warning'); - const [isIGVWindowOpen, setIsIGVWindowOpen] = useState(false); - const [showIGVButton, setShowIGVButton] = useState(false); - const [IGVOptions, setIGVOptions] = useState({}); - const [variantSearchOptions, setVariantSearchOptions] = useState({}); - const [patientList, setPatientList] = useState([]); - const [IGVBaseUrl, setIGVBaseUrl] = useState(`${htsget}/htsget/v1/variants/`); - - // Clinical Search Filter - const clinicalSearchPatients = useSelector((state) => state.customization.clinicalSearchResultPatients); - const clinicalSearch = useSelector((state) => state.customization.clinicalSearch); - const [cancerType, setCancerType] = React.useState([]); - - // Dropdown patient table open/closed - const [isListOpenMedications, setListOpenMedications] = React.useState(false); - const [isListOpenConditions, setListOpenConditions] = React.useState(false); - const [isListOpenSex, setListOpenSex] = React.useState(false); - const [isListOpenCancerType, setListOpenCancerType] = React.useState(false); - const [isListOpenHistological, setListOpenHistological] = React.useState(false); - - // Dropdown patient table filtering current selection in dropdown - const [selectedMedications, setSelectedMedications] = React.useState(clinicalSearch.clinicalSearchDropDowns.selectedMedications); - const [selectedConditions, setSelectedConditions] = React.useState(clinicalSearch.clinicalSearchDropDowns.selectedConditions); - const [selectedSex, setSelectedSex] = React.useState(clinicalSearch.clinicalSearchDropDowns.selectedSex); - const [selectedCancerType, setSelectedCancerType] = React.useState(clinicalSearch.clinicalSearchDropDowns.selectedCancerType); - const [selectedHistologicalType, setSelectedHistologicalType] = React.useState( - clinicalSearch.clinicalSearchDropDowns.selectedHistologicalType - ); - - // Dropdown patient table list for filtering - const [medicationList, setMedicationList] = React.useState(clinicalSearch.clinicalSearchDropDowns.medicationList); - const [conditionList, setConditionList] = React.useState(clinicalSearch.clinicalSearchDropDowns.conditionList); - const [sexList, setSexList] = React.useState(clinicalSearch.clinicalSearchDropDowns.sexList); - const [cancerTypeList, setCancerTypeList] = React.useState(clinicalSearch.clinicalSearchDropDowns.cancerTypeList); - const [HistologicalList, setHistologicalList] = React.useState(clinicalSearch.clinicalSearchDropDowns.HistologicalList); - - /* - Build the dropdown for chromosome - * @param {None} - * Return a list of options with chromosome - */ - function chrSelectBuilder() { - const refNameList = []; - ListOfReferenceNames.forEach((refName) => { - refNameList.push( - - {refName} - - ); - }); - return refNameList; - } - - function setClincalSearchPatients(data) { - dispatch({ - type: 'SET_CLINICAL_SEARCH_PATIENTS', - payload: { - data - } - }); - } - - function setRedux(rows) { - const tempClinicalSearchResults = []; - rows.forEach((patient) => { - tempClinicalSearchResults.push({ id: patient.id, genomicId: patient.genomic_id }); - }); - dispatch({ - type: 'SET_SELECTED_CLINICAL_SEARCH_RESULTS', - payload: { - selectedClinicalSearchResults: tempClinicalSearchResults, - clinicalSearchDropDowns: { - medicationList, - selectedMedications, - conditionList, - selectedConditions, - sexList, - selectedSex, - cancerTypeList, - selectedCancerType, - HistologicalList, - selectedHistologicalType - } - } - }); - } - - const dropDownSelection = (dropDownGroup, selected) => { - if (dropDownGroup === 'CONDITIONS') { - setSelectedConditions(selected); - setListOpenConditions(false); - } else if (dropDownGroup === 'MEDICATIONS') { - setSelectedMedications(selected); - setListOpenMedications(false); - } else if (dropDownGroup === 'SEX') { - setSelectedSex(selected); - setListOpenSex(false); - } else if (dropDownGroup === 'CANCER TYPE') { - setSelectedCancerType(selected); - setListOpenCancerType(false); - } else if (dropDownGroup === 'HISTOLOGICAL') { - setSelectedHistologicalType(selected); - setListOpenHistological(false); - } - }; - - // Filtering on cache - useEffect(() => { - if (Object.keys(clinicalSearchPatients.data).length !== 0) { - const tempRows = []; - const response = clinicalSearchPatients.data; - - for (let j = 0; j < response.results.length; j += 1) { - for (let i = 0; i < response.results[j].count; i += 1) { - // Patient table filtering - if ( - selectedConditions === 'All' && - selectedMedications === 'All' && - selectedSex === 'All' && - selectedCancerType === 'All' && - selectedHistologicalType === 'All' - ) { - // All patients - if (processMCodeMainData(response.results[j].results[i], response.results[j].location[0]).id !== null) { - tempRows.push(processMCodeMainData(response.results[j].results[i], response.results[j].location[0])); - } - } else { - // Filtered patients - let patientCondition = false; - response?.results[j]?.results[i]?.cancer_condition?.body_site?.every((bodySite) => { - if (selectedConditions === 'All' || selectedConditions === bodySite.label) { - patientCondition = true; - return false; - } - return true; - }); - let patientMedication = false; - response?.results[j]?.results[i]?.medication_statement.every((medication) => { - if (selectedMedications === 'All' || selectedMedications === medication?.medication_code.label) { - patientMedication = true; - return false; - } - return true; - }); - let patientSex = false; - if (selectedSex === 'All' || selectedSex === response?.results[j]?.results[i]?.subject.sex) { - patientSex = true; - } - let patientCancerType = false; - if (selectedCancerType === 'All') { - patientCancerType = true; - } else { - for (let k = 0; k < cancerType.length; k += 1) { - if ( - response?.results[j]?.results[i]?.cancer_condition?.code?.id !== undefined && - response?.results[j]?.results[i]?.cancer_condition?.code?.id === cancerType[k]['Cancer type code'] - ) { - if ( - selectedCancerType === - `${cancerType[k]['Cancer type label']} ${cancerType[k]['Cancer type code']}` || - selectedCancerType === 'NA' - ) { - patientCancerType = true; - } - } - } - } - let patientHistologicalType = false; - if (selectedHistologicalType === 'All') { - patientHistologicalType = true; - } else { - for (let k = 0; k < cancerType.length; k += 1) { - if ( - response?.results[j]?.results[i]?.cancer_condition?.histology_morphology_behavior?.id !== undefined && - response?.results[j]?.results[i]?.cancer_condition?.histology_morphology_behavior?.id === - cancerType[k]['Tumour histological type code'] - ) { - if ( - selectedHistologicalType === - `${cancerType[k]['Tumour histological type label']} ${cancerType[k]['Tumour histological type code']}` || - selectedHistologicalType === 'NA' - ) { - patientHistologicalType = true; - } - } - } - } - if ( - patientCondition && - patientMedication && - patientSex && - patientCancerType && - patientHistologicalType && - processMCodeMainData(response.results[j].results[i]).id !== null - ) { - tempRows.push(processMCodeMainData(response.results[j].results[i], response.results[j].location[0])); - } - } - } - } - const tempClinicalSearchResults = []; - tempRows.forEach((patient) => { - tempClinicalSearchResults.push({ id: patient.id, genomicId: patient.genomic_id }); - }); - - setPatientList(tempClinicalSearchResults); - // Dropdown patient table list for filtering - setMedicationList(processMedicationListData(response.results)); - setConditionList(processCondtionsListData(response.results)); - setSexList(processSexListData(response.results)); - setCancerTypeList(processCancerTypeListData(response.results)); - setHistologicalList(processHistologicalTypeListData(response.results)); - setLoading(false); - - setRedux(tempRows); - } - }, [selectedSex, selectedConditions, selectedMedications, selectedCancerType, selectedHistologicalType]); - - useEffect(() => { - setLoading(true); - setDisplayVariantsTable(false); - // get patient data from redux store or fetch it - if (Object.keys(clinicalSearch.selectedClinicalSearchResults).length !== 0) { - setPatientList(clinicalSearch.selectedClinicalSearchResults); - } - trackPromise( - fetchFederationClinicalData('/api/mcodepackets').then((response) => { - const patientData = []; - setClincalSearchPatients(response); - response.results.forEach((result) => { - result.results.forEach((patient) => { - patientData.push({ - id: patient.id, - genomicId: patient.genomics_report?.extra_properties?.genomic_id ?? 'NA' - }); - }); - }); - - const tempRows = []; - for (let j = 0; j < response.results.length; j += 1) { - for (let i = 0; i < response.results[j].count; i += 1) { - // Patient table filtering - if ( - selectedConditions === 'All' && - selectedMedications === 'All' && - selectedSex === 'All' && - selectedCancerType === 'All' && - selectedHistologicalType === 'All' - ) { - // All patients - if (processMCodeMainData(response.results[j].results[i], response.results[j].location[0]).id !== null) { - tempRows.push(processMCodeMainData(response.results[j].results[i], response.results[j].location[0])); - } - } else { - // Filtered patients - let patientCondition = false; - response?.results[j]?.results[i]?.cancer_condition?.body_site?.every((bodySite) => { - if (selectedConditions === 'All' || selectedConditions === bodySite.label) { - patientCondition = true; - return false; - } - return true; - }); - let patientMedication = false; - response?.results[j]?.results[i]?.medication_statement.every((medication) => { - if (selectedMedications === 'All' || selectedMedications === medication?.medication_code.label) { - patientMedication = true; - return false; - } - return true; - }); - let patientSex = false; - if (selectedSex === 'All' || selectedSex === response?.results[j]?.results[i]?.subject.sex) { - patientSex = true; - } - let patientCancerType = false; - if (selectedCancerType === 'All') { - patientCancerType = true; - } else { - for (let k = 0; k < cancerType.length; k += 1) { - if ( - response?.results[j]?.results[i]?.cancer_condition?.code?.id !== undefined && - response?.results[j]?.results[i]?.cancer_condition?.code?.id === cancerType[k]['Cancer type code'] - ) { - if ( - selectedCancerType === - `${cancerType[k]['Cancer type label']} ${cancerType[k]['Cancer type code']}` || - selectedCancerType === 'NA' - ) { - patientCancerType = true; - } - } - } - } - let patientHistologicalType = false; - if (selectedHistologicalType === 'All') { - patientHistologicalType = true; - } else { - for (let k = 0; k < cancerType.length; k += 1) { - if ( - response?.results[j]?.results[i]?.cancer_condition?.histology_morphology_behavior?.id !== - undefined && - response?.results[j]?.results[i]?.cancer_condition?.histology_morphology_behavior?.id === - cancerType[k]['Tumour histological type code'] - ) { - if ( - selectedHistologicalType === - `${cancerType[k]['Tumour histological type label']} ${cancerType[k]['Tumour histological type code']}` || - selectedHistologicalType === 'NA' - ) { - patientHistologicalType = true; - } - } - } - } - if ( - patientCondition && - patientMedication && - patientSex && - patientCancerType && - patientHistologicalType && - processMCodeMainData(response.results[j].results[i]).id !== null - ) { - tempRows.push(processMCodeMainData(response.results[j].results[i], response.results[j].location[0])); - } - } - } - } - const tempClinicalSearchResults = []; - tempRows.forEach((patient) => { - tempClinicalSearchResults.push({ id: patient.id, genomicId: patient.genomic_id }); - }); - - setPatientList(tempClinicalSearchResults ?? patientData); - // Dropdown patient table list for filtering - setMedicationList(processMedicationListData(response.results)); - setConditionList(processCondtionsListData(response.results)); - setSexList(processSexListData(response.results)); - setCancerTypeList(processCancerTypeListData(response.results)); - setHistologicalList(processHistologicalTypeListData(response.results)); - setLoading(false); - - setRedux(tempRows); - }), - 'patientBox' - ); - }, []); - - /** - * This function handles the Search button - * It calls the searchVariant function from api.js according to the chromosome and position - * then compares the result with the patientList genomicID to find the matching patient - */ - const formHandler = (e) => { - e.preventDefault(); // Prevent form submission - setDisplayVariantsTable(false); - const referenceName = e.target.genome.value; - const chromosome = e.target.chromosome.value; - const start = e.target.start.value; - const end = e.target.end.value; - setVariantSearchOptions({ referenceName, chromosome, start, end }); - trackPromise( - searchVariant(chromosome, start, end) - .then((response) => { - const locationObjs = response.results; - if (Object.keys(locationObjs).length === 0) { - setAlertMessage('No variants found'); - setAlertSeverity('warning'); - setOpen(true); - } else { - const variantList = []; - locationObjs.forEach((location) => { - location.results.forEach((item) => - variantList.push({ - id: item.id, - genomicId: item.genomic_id, - locationName: location.location[0], - referenceName: item.reference_genome, - variantCount: item.variantcount, - samples: item.samples, - url: item.urls[0] // only one url unless the requirement changes - }) - ); - }); - // Build display table - const displayData = []; - for (let i = 0; i < variantList.length; i += 1) { - for (let j = 0; j < patientList.length; j += 1) { - if (variantList[i].genomicId === patientList[j].genomicId) { - displayData.push({ - patientId: patientList[j].id, - locationName: variantList[i].locationName, - genomicSampleId: variantList[i].genomicId, - variantCount: variantList[i].variantCount, - samples: variantList[i].samples, - VCFFile: variantList[i].id - }); - // get the url part before the file name for IGV - const url = variantList[i].url; - const urlParts = url.substring(0, url.lastIndexOf('/') + 1); - setIGVBaseUrl(urlParts); - break; // should have only 1 match, so we can break here - } - } - } - setRowData(displayData); - setDisplayVariantsTable(true); - } - }) - .catch((error) => { - setAlertMessage(error.message); - setAlertSeverity('error'); - setOpen(true); - }), - 'table' - ); - }; - - const toggleIGVWindow = () => { - setIsIGVWindowOpen(!isIGVWindowOpen); - }; - - /** - * This function keeps track of the selected row in the table - * then it creates the IGV options to send to the IGV window - */ - const onCheckboxSelectionChanged = (value) => { - if (value.length === 0) { - setShowIGVButton(false); - } else { - setShowIGVButton(true); - } - const trackList = []; - for (let i = 0; i < value.length; i += 1) { - trackList.push({ - name: value[i]['Patient ID'], - type: 'variant', - format: 'vcf', - sourceType: 'htsget', - url: `${IGVBaseUrl}${value[i]['VCF File']}` - }); - } - const options = { - genome: 'hg38', - locus: `${variantSearchOptions.chromosome}:${variantSearchOptions.start}-${variantSearchOptions.end}`, - tracks: trackList - }; - setIGVOptions(options); - }; - - return ( - <> - - {!isLoading ? ( - - } spacing={2}> - - - Total Patients - - - {patientList.length} - - - } spacing={2}> - - - Reference Genome - - gh38 - gh37 - - - - - - Chromosome - - {chrSelectBuilder()} - - - - - - Start - - - - - - End - - - - - - - Search - - - - - - - - ) : ( - - )} - - {displayVariantsTable ? ( - - ) : ( - - )} - - {isIGVWindowOpen && } - > - ); -} - -export default VariantsSearch; diff --git a/src/views/ingest/ingest.js b/src/views/ingest/ingest.js new file mode 100644 index 00000000..8c9341e9 --- /dev/null +++ b/src/views/ingest/ingest.js @@ -0,0 +1,42 @@ +import IngestMenu from 'ui-component/ingest/IngestMenu'; +import { Grid } from '@mui/material'; +import { makeStyles } from '@mui/styles'; + +function IngestPage() { + const useStyles = makeStyles({ + container: { + color: 'white', + background: 'white', + height: '100%', + width: '100%', + paddingTop: '0.8em', + borderRadius: '1em', + overflow: 'hidden', + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'flex-start', + display: 'inline-flex' + }, + label: { + color: 'black', + fontSize: 24, + fontFamily: 'Roboto', + fontWeight: '700', + wordWrap: 'break-word', + marginLeft: '0.65em', + marginBottom: '1em', + marginTop: '0.51em' + } + }); + const classes = useStyles(); + return ( + + + Ingest Data + + + + ); +} + +export default IngestPage; diff --git a/src/views/summary/CustomOfflineChart.js b/src/views/summary/CustomOfflineChart.js index 85262bef..6e3eccaf 100644 --- a/src/views/summary/CustomOfflineChart.js +++ b/src/views/summary/CustomOfflineChart.js @@ -1,4 +1,4 @@ -import { createRef, useCallback, useState, useEffect } from 'react'; +import { createRef, useState, useEffect } from 'react'; // MUI import PropTypes from 'prop-types'; @@ -9,8 +9,7 @@ import { Box, IconButton } from '@mui/material'; import { useSelector } from 'react-redux'; // Third-party libraries -import Cookies from 'js-cookie'; -import Highcharts, { map } from 'highcharts'; +import Highcharts from 'highcharts'; import HighchartsReact from 'highcharts-react-official'; import NoDataToDisplay from 'highcharts/modules/no-data-to-display'; import { IconTrash } from '@tabler/icons'; @@ -158,7 +157,7 @@ function CustomOfflineChart(props) { legend: { enabled: false }, series: stackSeries, tooltip: { - pointFormat: '{point.name}: {point.y}' + pointFormat: '{series.name}: {point.y}' } }); } else if (validCharts.includes(chart)) { @@ -198,14 +197,17 @@ function CustomOfflineChart(props) { series: [{ data, colorByPoint: true, showInLegend: false }], tooltip: { useHTML: true, + // Anonymous functions don't appear to work with highcharts for some reason? + /* eslint-disable func-names */ formatter: function () { let dataSum = 0; this.series.points.forEach((point) => { dataSum += point.y; }); const pcnt = (this.y / dataSum) * 100; - return ` ${this.point.category} - ${this.y} (${Highcharts.numberFormat(pcnt)}%) total patient(s)`; + return ` ${this.key} - ${this.y} (${Highcharts.numberFormat(pcnt)}%) total patient(s)`; } + /* eslint-enable func-names */ } }); } else { @@ -255,27 +257,42 @@ function CustomOfflineChart(props) { } createChart(); - }, [dataVis, chart, chartData, JSON.stringify(dataObject), trim]); + }, [ + dataVis, + chart, + chartData, + trim, + cutoff, + dataObject, + grayscale, + height, + orderAlphabetically, + orderByFrequency, + theme.palette.grey, + theme.palette.primary, + theme.palette.secondary, + theme.palette.tertiary + ]); - function setCookieDataVisChart(event) { - // Set cookie for Data Visualization Chart Type - const dataVisChart = JSON.parse(Cookies.get('dataVisChartType')); + function setLocalStorageDataVisChart(event) { + // Set LocalStorage for Data Visualization Chart Type + const dataVisChart = JSON.parse(localStorage.getItem('dataVisChartType')); dataVisChart[index] = event.target.value; - Cookies.set('dataVisChartType', JSON.stringify(dataVisChart), { expires: 365 }); + localStorage.setItem('dataVisChartType', JSON.stringify(dataVisChart), { expires: 365 }); } - function setCookieDataVisData(event) { - // Set Cookie for Data Visualization Data - const dataVisData = JSON.parse(Cookies.get('dataVisData')); + function setLocalStorageDataVisData(event) { + // Set LocalStorage for Data Visualization Data + const dataVisData = JSON.parse(localStorage.getItem('dataVisData')); dataVisData[index] = event.target.value; - Cookies.set('dataVisData', JSON.stringify(dataVisData), { expires: 365 }); + localStorage.setItem('dataVisData', JSON.stringify(dataVisData), { expires: 365 }); } - function setCookieDataVisTrim(value) { - // Set Cookie for Data Visualization Trim status - const dataVisTrim = JSON.parse(Cookies.get('dataVisTrim')); + function setLocalStorageDataVisTrim(value) { + // Set LocalStorage for Data Visualization Trim status + const dataVisTrim = JSON.parse(localStorage.getItem('dataVisTrim')); dataVisTrim[index] = value; - Cookies.set('dataVisTrim', JSON.stringify(dataVisTrim), { expires: 365 }); + localStorage.setItem('dataVisTrim', JSON.stringify(dataVisTrim), { expires: 365 }); } /* eslint-disable jsx-a11y/no-onchange */ @@ -289,7 +306,7 @@ function CustomOfflineChart(props) { } else { chartObj?.hideLoading(); } - }, [loading]); + }, [chartRef, loading]); const showTrim = (dataObject || dataVis[chartData]) && Object.entries(dataObject === '' ? dataVis[chartData] : dataObject).length > 15; @@ -329,7 +346,7 @@ function CustomOfflineChart(props) { id="types" onChange={(event) => { setChartData(event.target.value); - setCookieDataVisData(event); + setLocalStorageDataVisData(event); }} > {Object.keys(dataVis).map((key) => ( @@ -350,7 +367,7 @@ function CustomOfflineChart(props) { id="types" onChange={(event) => { setChart(event.target.value); - setCookieDataVisChart(event); + setLocalStorageDataVisChart(event); }} > Stacked Bar @@ -365,7 +382,7 @@ function CustomOfflineChart(props) { id="types" onChange={(event) => { setChart(event.target.value); - setCookieDataVisChart(event); + setLocalStorageDataVisChart(event); }} > Bar @@ -382,7 +399,7 @@ function CustomOfflineChart(props) { type="checkbox" id="trim" onChange={() => { - setCookieDataVisTrim(!trim); + setLocalStorageDataVisTrim(!trim); setTrim((old) => !old); }} checked={trim} @@ -398,15 +415,20 @@ function CustomOfflineChart(props) { } CustomOfflineChart.propTypes = { + chartType: PropTypes.string, + cutoff: PropTypes.number, + data: PropTypes.string, + dataObject: PropTypes.any, + dataVis: PropTypes.any, dropDown: PropTypes.bool, + edit: PropTypes.bool, + grayscale: PropTypes.bool, height: PropTypes.string, - dataVis: PropTypes.any, - dataObject: PropTypes.any, + index: PropTypes.number, + loading: PropTypes.bool, onRemoveChart: PropTypes.func, - grayscale: PropTypes.bool, orderByFrequency: PropTypes.bool, orderAlphabetically: PropTypes.bool, - cutoff: PropTypes.number, trimByDefault: PropTypes.bool }; diff --git a/src/views/summary/TreatingCentreMap.js b/src/views/summary/TreatingCentreMap.js index 70607cb7..c8ccb716 100644 --- a/src/views/summary/TreatingCentreMap.js +++ b/src/views/summary/TreatingCentreMap.js @@ -5,9 +5,6 @@ import HighchartsMap from 'highcharts/modules/map'; import mapDataCanada from '@highcharts/map-collection/countries/ca/ca-all.geo.json'; import PropTypes from 'prop-types'; -// mui -import { useTheme } from '@mui/styles'; - import { LoadingIndicator, usePromiseTracker, trackPromise } from 'ui-component/LoadingIndicator/LoadingIndicator'; import MainCard from 'ui-component/cards/MainCard'; diff --git a/src/views/summary/summary.js b/src/views/summary/summary.js index ea60bb7d..af33c0eb 100644 --- a/src/views/summary/summary.js +++ b/src/views/summary/summary.js @@ -44,7 +44,7 @@ function Summary() { const sidebarWriter = useSidebarWriterContext(); useEffect(() => { sidebarWriter(null); - }, []); + }, [sidebarWriter]); /* Aggregated count of federated data */ function federationStatCount(data, endpoint) { diff --git a/src/views/utilities/Color.js b/src/views/utilities/Color.js index d92b9206..d41e88b3 100644 --- a/src/views/utilities/Color.js +++ b/src/views/utilities/Color.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import React from 'react'; // mui import { useTheme } from '@mui/styles'; diff --git a/src/views/utilities/MaterialIcons.js b/src/views/utilities/MaterialIcons.js index b4674af1..538369d8 100644 --- a/src/views/utilities/MaterialIcons.js +++ b/src/views/utilities/MaterialIcons.js @@ -1,5 +1,3 @@ -import React from 'react'; - // mui import { makeStyles } from '@mui/styles'; import { Card } from '@mui/material'; diff --git a/src/views/utilities/Shadow.js b/src/views/utilities/Shadow.js index 442e9d61..3603760c 100644 --- a/src/views/utilities/Shadow.js +++ b/src/views/utilities/Shadow.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import React from 'react'; // mui import { useTheme } from '@mui/styles'; diff --git a/src/views/utilities/TablerIcons.js b/src/views/utilities/TablerIcons.js index 61b3bff0..3b9a965a 100644 --- a/src/views/utilities/TablerIcons.js +++ b/src/views/utilities/TablerIcons.js @@ -1,5 +1,3 @@ -import React from 'react'; - // mui import { makeStyles } from '@mui/styles'; import { Card } from '@mui/material'; diff --git a/src/views/utilities/Typography.js b/src/views/utilities/Typography.js index 56fa8bf3..54cd234b 100644 --- a/src/views/utilities/Typography.js +++ b/src/views/utilities/Typography.js @@ -1,4 +1,3 @@ -import React from 'react'; // mui import { Grid, Link } from '@mui/material'; import MuiTypography from '@mui/material/Typography';