Skip to content

Commit

Permalink
Feature/patient info (#110)
Browse files Browse the repository at this point in the history
* Ingest menu button
* Basic ingest page tab navigation
* Update package-lock.json
* Ingest page code style fixes
* Remove permissions tab
* Add navigation button
* Basic clinical ingest page skeleton
* Basic genomic ingest page skeleton
* File upload only accept JSON
* Persistent file dialogs
* Validate JSON upload
* Rework clinical&genomic data to work with the new query microservice
* Sidebar merging, table, topLevel
* Patient info
Htsget call, dyanmic sidebar, dynamic table and parent ID, folders
* Fix parent ID addition in the table
* Katsu UUID changes, minor styling adjustment
* ESlint fixes, pullout functions, PR review changes
* Eslint and empty object fix
* Fix up the datarow componnent
* Quick fix to a warning that's been bugging me for forever
* styling and develop fixing
* Selection highlight in sidebar
* Sidebar height matching
* Update package-lock.json
* Prettier linter changes
* ESlint fixes
* Top level json fix
* prettier fix


---------

Signed-off-by: Courtney Gosselin <[email protected]>
Co-authored-by: Justin <[email protected]>
Co-authored-by: fnguyen <[email protected]>
  • Loading branch information
3 people authored Jan 17, 2024
1 parent 0432dfa commit f701116
Show file tree
Hide file tree
Showing 10 changed files with 468 additions and 14 deletions.
4 changes: 2 additions & 2 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
REACT_APP_KATSU_API_SERVER=''
REACT_APP_FEDERATION_API_SERVER=''
REACT_APP_KATSU_API_SERVER='candig.docker.internal:5080/katsu/v2/'
REACT_APP_FEDERATION_API_SERVER='candig.docker.internal:5080'
REACT_APP_BASE_NAME=''
REACT_APP_SITE_LOCATION = ''
REACT_APP_HTSGET_SERVER = ''
Expand Down
7 changes: 3 additions & 4 deletions src/layout/MainLayout/Sidebar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ const Root = styled('nav')(({ theme }) => ({
}));

// ===========================|| SIDEBAR DRAWER ||=========================== //

function Sidebar({ drawerOpen, drawerToggle, window }) {
function Sidebar({ drawerOpen, drawerToggle, screen }) {
const theme = useTheme();
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'));
const sidebarContext = useSidebarReaderContext();
Expand All @@ -86,7 +85,7 @@ function Sidebar({ drawerOpen, drawerToggle, window }) {
</>
);

const container = window !== undefined ? () => window().document.body : undefined;
const container = screen !== undefined ? () => window().document.body : undefined;

return (
<Root className={classes.drawer} aria-label="mailbox folders">
Expand All @@ -111,7 +110,7 @@ function Sidebar({ drawerOpen, drawerToggle, window }) {
Sidebar.propTypes = {
drawerOpen: PropTypes.bool,
drawerToggle: PropTypes.func,
window: PropTypes.object
screen: PropTypes.object
};

export default Sidebar;
12 changes: 12 additions & 0 deletions src/routes/MainRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const Summary = Loadable(lazy(() => import('views/summary/summary')));

// Clinical & Genomic Search
const ClinicalGenomicSearch = Loadable(lazy(() => import('views/clinicalGenomic/clinicalGenomicSearch')));
const ClinicalPatientView = Loadable(lazy(() => import('views/clinicalGenomic/clinicalPatientView')));

// Ingest Portal
const IngestPortal = Loadable(lazy(() => import('views/ingest/ingest')));

// Ingest Portal
// const IngestPortal = Loadable(lazy(() => import('views/ingest/ingest')));
Expand Down Expand Up @@ -48,6 +52,14 @@ const MainRoutes = {
path: `${basename}/data-ingest`,
element: <IngestPortal />
}, */
{
path: `${basename}/patientView`,
element: <ClinicalPatientView />
},
{
path: `${basename}/frontendIngest`,
element: <IngestPortal />
},
{
path: '*',
element: <ErrorNotFoundPage />
Expand Down
26 changes: 26 additions & 0 deletions src/store/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,32 @@ export function fetchKatsu(URL) {
});
}

/*
Fetch htsget calls
*/
export function fetchHtsget() {
return fetch(`${federation}/fanout`, {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
method: 'GET',
path: `ga4gh/drs/v1/objects`,
payload: {},
service: 'htsget'
})
})
.then((response) => {
if (response.ok) {
return response.json();
}
return [];
})
.catch((error) => {
console.log(`Error: ${error}`);
return 'error';
});
}

export function fetchFederationStat(endpoint) {
return fetch(`${federation}/fanout`, {
method: 'post',
Expand Down
12 changes: 12 additions & 0 deletions src/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,15 @@ export function mergeFederatedResults(data) {
}
return output;
}

/*
* Format a key by converting underscores to spaces and capitalizing each word
* @param {string} key - The key to be formatted
* @returns {string} - The formatted key
*/
export function formatKey(key) {
return key
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
6 changes: 0 additions & 6 deletions src/views/clinicalGenomic/clinicalGenomicSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import MainCard from 'ui-component/cards/MainCard';
import PatientCounts from './widgets/patientCounts';
import DataVisualization from './widgets/dataVisualization';
import ClinicalData from './widgets/clinicalData';
import PatientView from './widgets/patientView';
import { useSidebarWriterContext } from '../../layout/MainLayout/Sidebar/SidebarContext';
import Sidebar from './widgets/sidebar';
import { COHORTS } from 'store/constant';
Expand Down Expand Up @@ -80,11 +79,6 @@ const sections = [
header: 'Clinical Data',
component: <ClinicalData />
},
{
id: 'patient',
header: 'Patient View',
component: <PatientView />
},
{
id: 'genomic',
header: 'Genomic Data',
Expand Down
74 changes: 74 additions & 0 deletions src/views/clinicalGenomic/clinicalPatientView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState, useEffect } from 'react';
import { styled } from '@mui/system';
import { Box, Typography } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import { useSelector } from 'react-redux';
import clsx from 'clsx';

import MainCard from 'ui-component/cards/MainCard';
import useClinicalPatientData from './useClinicalPatientData';
import { formatKey } from '../../utils/utils';

const StyledTopLevelBox = styled(Box)(({ theme }) => ({
border: `1px solid ${theme.palette.primary.main}`,
marginTop: '1em',
padding: '1em',
borderBottomLeftRadius: '10px',
borderBottomRightRadius: '10px',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
gap: '1em',
boxShadow: '5px 5px 10px rgba(0, 0, 0, 0.2)'
}));

function ClinicalPatientView() {
const { customization } = useSelector((state) => state);

const [patientId, setPatientId] = useState('');
const [programId, setProgramId] = useState('');
const { rows, columns, title, topLevel } = useClinicalPatientData(patientId, programId);

useEffect(() => {
// Extract patientId from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const initialPatientId = urlParams.get('patientId');
const intitalProgramId = urlParams.get('programId');

setPatientId(initialPatientId || '');
setProgramId(intitalProgramId || '');
}, []);

const additionalClass = 'your-additional-class'; // Replace with your actual class

return (
<MainCard sx={{ borderRadius: customization.borderRadius * 0.25, margin: 0 }}>
<Typography pb={1} variant="h5" style={{ fontWeight: 'bold' }}>
{title}
</Typography>
<Typography pb={1} variant="h6">
{patientId}
</Typography>
<div style={{ width: '100%', height: '68vh' }}>
<DataGrid rows={rows} columns={columns} pageSize={10} rowsPerPageOptions={[10]} hideFooterSelectedRowCount />
</div>
<StyledTopLevelBox className={clsx(additionalClass)}>
{Object.entries(topLevel).map(([key, value]) => (
<div
key={key}
style={{
display: 'flex',
flexDirection: 'row',
gap: '0.5em'
}}
>
<p style={{ fontWeight: 'bold', margin: 0 }}>{formatKey(key)}:</p>
<p style={{ margin: 0 }}>{String(value)}</p>
</div>
))}
</StyledTopLevelBox>
</MainCard>
);
}

export default ClinicalPatientView;
69 changes: 69 additions & 0 deletions src/views/clinicalGenomic/useClinicalPatientData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useEffect, useState } from 'react';
import { useSidebarWriterContext } from '../../layout/MainLayout/Sidebar/SidebarContext';
import { fetchFederation } from '../../store/api';
import PatientSidebar from './widgets/patientSidebar';

/*
* Custom hook to fetch and manage clinical patient data.
* @param {string} patientId - The ID of the patient.
* @param {string} programId - The ID of the program.
* @returns {Object} - An object containing data, rows, columns, title, and topLevel.
*/
function useClinicalPatientData(patientId, programId) {
// Access the SidebarContext to update the sidebar with patient information
const sidebarWriter = useSidebarWriterContext();

// State variables to store fetched data, table rows, columns, title, and topLevel data
const [data, setData] = useState({});
const [rows, setRows] = useState([]);
const [columns, setColumns] = useState([]);
const [title, setTitle] = useState('');
const [topLevel, setTopLevel] = useState({});

function filterNestedObject(obj) {
return Object.fromEntries(
Object.entries(obj).filter(
([key, value]) =>
value !== null &&
!(
(Array.isArray(value) && value.length === 0) || // Exclude empty arrays
typeof value === 'object' || // Exclude all objects
value === '' ||
key === ''
)
)
);
}

// useEffect to fetch data when patientId, programId, or sidebarWriter changes
useEffect(() => {
// Asynchronous function to fetch data
const fetchData = async () => {
try {
// Construct the API URL based on the provided parameters
if (programId && patientId) {
const url = `v2/authorized/donor_with_clinical_data/program/${programId}/donor/${patientId}`;

const result = await fetchFederation(url, 'katsu');
// Extract patientData from the fetched result or use an empty object
const patientData = result[0].results || {};

// Update the sidebar with patientData using the PatientSidebar component
sidebarWriter(<PatientSidebar sidebar={patientData} setRows={setRows} setColumns={setColumns} setTitle={setTitle} />);
// Filter patientData to create topLevel data excluding arrays, objects, and empty values
const filteredData = filterNestedObject(patientData);
setTopLevel(filteredData);
setData(patientData);
}
} catch (error) {
console.error('Error fetching clinical patient data:', error);
}
};

fetchData();
}, [patientId, programId, sidebarWriter]);

return { data, rows, columns, title, topLevel };
}

export default useClinicalPatientData;
4 changes: 2 additions & 2 deletions src/views/clinicalGenomic/widgets/clinicalData.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ function ClinicalView() {

// Mobile
const [desktopResolution, setdesktopResolution] = React.useState(window.innerWidth > 1200);

const searchResults = useSearchResultsReaderContext().clinical;
const writerContext = useSearchQueryWriterContext();

Expand All @@ -40,7 +39,8 @@ function ClinicalView() {
}

const handleRowClick = (row) => {
writerContext((old) => ({ ...old, donorID: row.submitter_donor_id, cohort: row.program_id }));
const url = `/patientView?patientId=${row.submitter_donor_id}&programId=${row.program_id}`;
window.open(url, '_blank');
};

// Tracks Screensize
Expand Down
Loading

0 comments on commit f701116

Please sign in to comment.