Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

NickAkhmetov/HMP-180 Cell types landing page first pass #3130

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG-hmp-180.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Added first pass implementation of cell types page.
- Added initial implementations of individual cell-type description, assays, dataset, and organ lookups.
3 changes: 2 additions & 1 deletion context/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from . import (
routes_main, routes_browse, routes_api, routes_file_based,
routes_auth, routes_cells, routes_markdown, routes_notebooks,
routes_workspaces, default_config)
routes_workspaces, routes_cell_types, default_config)
from .flask_static_digest import FlaskStaticDigest
flask_static_digest = FlaskStaticDigest()

Expand Down Expand Up @@ -71,6 +71,7 @@ def create_app(testing=False):
app.register_blueprint(routes_markdown.blueprint)
app.register_blueprint(routes_notebooks.blueprint)
app.register_blueprint(routes_workspaces.blueprint)
app.register_blueprint(routes_cell_types.blueprint)

app.register_error_handler(400, bad_request)
app.register_error_handler(401, unauthorized)
Expand Down
86 changes: 86 additions & 0 deletions context/app/routes_cell_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from itertools import islice, groupby

from flask import render_template, current_app, request, jsonify

from hubmap_api_py_client import Client
from hubmap_api_py_client.errors import ClientError
import requests

from .utils import get_default_flask_data, make_blueprint, get_organs

blueprint = make_blueprint(__name__)


def _get_client(app):
return Client(app.config['XMODALITY_ENDPOINT'] + '/api/')


@blueprint.route('/cell-types')
def cell_types_ui():
organs = get_organs()
return render_template(
'base-pages/react-content.html',
title='Cell Types',
flask_data={**get_default_flask_data(), 'organs': organs}
)


# Fetches list of all cell types
@blueprint.route('/cell-types/list.json')
def cell_types_list():
celltype_token = requests.post(
'https://cells.api.hubmapconsortium.org/api/celltype/', {}).json()['results'][0]['query_handle']
celltype_list = [result['grouping_name'] for result in requests.post('https://cells.api.hubmapconsortium.org/api/celltypeevaluation/', {
'key': celltype_token, 'set_type': 'cell_type', 'limit': 500}).json()['results']]
return jsonify(celltype_list)


# Fetches cell type description
@blueprint.route('/cell-types/<cell_type>/description.json')
def cell_type_description(cell_type):
headers = {"accept": 'application/json'}
celltype_concepts = requests.get(
f'https://ontology.api.hubmapconsortium.org/terms/{cell_type}/concepts', headers=headers).json()
if (len(celltype_concepts) == 0):
return jsonify('No description available')
concept = celltype_concepts[0]
description = requests.get(
f'https://ontology.api.hubmapconsortium.org/concepts/{concept}/definitions', headers=headers).json()
if (len(description) == 0):
return jsonify('No description available')
return jsonify(description[0]['definition'])
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved


# Fetches dataset UUIDs for a given cell type
@blueprint.route('/cell-types/<cell_type>/datasets.json')
def cell_type_datasets(cell_type):
client = _get_client(current_app)
datasets = client.select_datasets(where='celltype', has=[cell_type]).get_list()
datasets = [dataset['uuid'] for dataset in list(datasets)]

return jsonify(datasets)


# Fetches list of organs for a given cell type
@blueprint.route('/cell-types/<cell_type>/organs.json')
def cell_type_organs(cell_type):
client = _get_client(current_app)
organs = client.select_organs(where='celltype', has=[cell_type]).get_list()
organs = [organ['grouping_name'] for organ in list(organs)]
organs = ', '.join([organ for organ in list(organs)])

return jsonify(organs)


# Fetches list of assays for a given cell type
@blueprint.route('/cell-types/<cell_type>/assays.json')
def cell_type_assays(cell_type):
# TODO: Uncomment when assays are available
# client = _get_client(current_app)
# assays = [assay['grouping_name']
# for assay in client.select_assays(where='celltype', has=[cell_type]).get_list()]
# assays = ','.join([dataset for dataset in list(assays)])

assays = ', '.join(['TBD'])

return jsonify(assays)
9 changes: 9 additions & 0 deletions context/app/static/js/components/Routes/Routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Error from 'js/pages/Error';
import Route from './Route';
import useSendPageView from './useSendPageView';
import useSetUrlBeforeLogin from './useSetUrlBeforeLogin';
import CellTypes from '../../pages/CellTypes';

const Donor = lazy(() => import('js/pages/Donor'));
const Dataset = lazy(() => import('js/pages/Dataset'));
Expand Down Expand Up @@ -280,6 +281,14 @@ function Routes({ flaskData }) {
);
}

if (urlPath.startsWith('/cell-types')) {
return (
<Route>
<CellTypes />
</Route>
);
}

if ('markdown' in flaskData) {
return (
<Route>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { useState, useEffect } from 'react';

import DialogModal from 'js/shared-styles/DialogModal/DialogModal';
import { DatasetIcon } from 'js/shared-styles/icons';

import Skeleton from '@material-ui/lab/Skeleton/Skeleton';
import { useCellTypeDatasets } from './hooks';
import { ViewDatasetsButton } from './style';
import PanelList from '../../shared-styles/panels/PanelList/PanelList';

const CellTypeDatasetsModal = ({ cellType }) => {
const [isOpen, setIsOpen] = useState(false);
const [hasBeenOpened, setHasBeenOpened] = useState(false);

const { datasets, isLoading } = useCellTypeDatasets(cellType, hasBeenOpened);

useEffect(() => {
if (isOpen) {
setHasBeenOpened(true);
}
}, [isOpen]);

return (
<>
<ViewDatasetsButton onClick={() => setIsOpen(true)} startIcon={<DatasetIcon />} variant="outlined">
View&nbsp;Datasets
</ViewDatasetsButton>
<DialogModal
isOpen={isOpen}
handleClose={() => setIsOpen(false)}
title={`Datasets with ${cellType}`}
content={
<>
{isLoading && <Skeleton variant="text" width="100%" />}
{datasets && (
<PanelList
panelsProps={datasets?.map((dataset) => ({ title: dataset, href: `/browse/dataset/${dataset}` }))}
/>
)}
</>
}
/>
</>
);
};

export default CellTypeDatasetsModal;
34 changes: 34 additions & 0 deletions context/app/static/js/components/cellTypes/CellTypeHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';

import Typography from '@material-ui/core/Typography';
import Skeleton from '@material-ui/lab/Skeleton';
import Description from 'js/shared-styles/sections/Description';
import theme from 'js/theme';

import { PageSectionContainer } from './style';
import { useCellTypesList } from './hooks';

const CellTypeHeader = () => {
const { cellTypes } = useCellTypesList();
return (
<PageSectionContainer>
<Typography variant="h2" component="h1">
Cell Types
</Typography>
<Typography variant="subtitle1" color="primary">
{cellTypes ? cellTypes.length : <Skeleton variant="text" />} Cell Types
</Typography>
<Description padding={`${theme.spacing(2)}px`} withIcon={false}>
Navigate the cell types available in the HuBMAP portal.
</Description>
<Description padding={`${theme.spacing(2)}px`}>
To filter the cell type list in the table below by organ, select organ(s) in the anatomical view on the left or
select from the list provided on the right by selecting the card. Selecting a cell type will navigate you to
that cell type page with additional information about the cell type including visualizations and a list of
HuBMAP data affiliated with that cell type.
</Description>
</PageSectionContainer>
);
};

export default CellTypeHeader;
23 changes: 23 additions & 0 deletions context/app/static/js/components/cellTypes/CellTypeOrgans.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';

import OrganTile from 'js/components/organ/OrganTile';
import { useFlaskDataContext } from 'js/components/Contexts';
import { Refresh } from 'js/shared-styles/icons';

import { PageSectionContainer, OrganTilesContainer, CellTypesButton } from './style';

const CellTypeOrgans = () => {
const { organs } = useFlaskDataContext();
return (
<PageSectionContainer>
<CellTypesButton startIcon={<Refresh />}>Reset Filters</CellTypesButton>
<OrganTilesContainer>
{Object.entries(organs).map(([name, organ]) => (
<OrganTile key={name} organ={organ} />
))}
</OrganTilesContainer>
</PageSectionContainer>
);
};

export default CellTypeOrgans;
40 changes: 40 additions & 0 deletions context/app/static/js/components/cellTypes/CellTypeRow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';

import Skeleton from '@material-ui/lab/Skeleton';

import { useInView } from 'react-intersection-observer';

import { StyledTableRow, StyledTableCell } from 'js/components/searchPage/ResultsTable/style';
import { useCellTypeDescription, useCellTypeOrgans, useCellTypeAssays } from './hooks';
import CellTypeDatasetsModal from './CellTypeDatasetsModal';

// type CellTypeRowProps = {
// cellType: string;
// }

const CellTypeRow = ({ cellType }) => {
// Delay loading description/organ information until row is in view
const { ref, inView } = useInView({
threshold: 0,
initialInView: false,
triggerOnce: true,
});

const { description, isLoading: descriptionIsLoading } = useCellTypeDescription(cellType, inView);
const { organs, isLoading: organsAreLoading } = useCellTypeOrgans(cellType, inView);
const { assays, isLoading: assaysAreLoading } = useCellTypeAssays(cellType, inView);

return (
<StyledTableRow ref={ref}>
<StyledTableCell>{cellType}</StyledTableCell>
<StyledTableCell>{descriptionIsLoading ? <Skeleton /> : description}</StyledTableCell>
<StyledTableCell>{organsAreLoading ? <Skeleton /> : organs}</StyledTableCell>
<StyledTableCell>{assaysAreLoading ? <Skeleton /> : assays}</StyledTableCell>
<StyledTableCell align="right">
<CellTypeDatasetsModal cellType={cellType} />
</StyledTableCell>
</StyledTableRow>
);
};

export default CellTypeRow;
60 changes: 60 additions & 0 deletions context/app/static/js/components/cellTypes/CellTypeTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import TableHead from '@material-ui/core/TableHead';
import Typography from '@material-ui/core/Typography';

import {
StyledTable,
StyledTableBody,
StyledTableRow,
StyledTableCell,
} from 'js/components/searchPage/ResultsTable/style';
import SearchBarInput from 'js/shared-styles/inputs/SearchBar';
import { FilterList } from 'js/shared-styles/icons';

import Skeleton from '@material-ui/lab/Skeleton/Skeleton';
import { useCellTypesList } from './hooks';
import { PageSectionContainer, CellTypesButton } from './style';
import CellTypeRow from './CellTypeRow';

const CellTypesTable = () => {
const { cellTypes } = useCellTypesList();
return (
<PageSectionContainer>
<CellTypesButton startIcon={<FilterList />}>Additional Filters</CellTypesButton>
<SearchBarInput placeholder="Search Cell Type" />
<StyledTable>
<TableHead>
<StyledTableRow>
<StyledTableCell>Cell Type</StyledTableCell>
<StyledTableCell>Description</StyledTableCell>
<StyledTableCell>Organs</StyledTableCell>
<StyledTableCell>Assays</StyledTableCell>
<StyledTableCell>
<Typography variant="srOnly">Datasets</Typography>
</StyledTableCell>
</StyledTableRow>
</TableHead>
<StyledTableBody>
{cellTypes?.map((cellType) => (
<CellTypeRow key={cellType} cellType={cellType} />
))}
{!cellTypes && <TablePlaceholder />}
</StyledTableBody>
</StyledTable>
</PageSectionContainer>
);
};

const placeholderArray = new Array(10).fill(0);

const TablePlaceholder = () =>
placeholderArray.map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<StyledTableRow key={index}>
<StyledTableCell colSpan={5}>
<Skeleton variant="text" width="100%" />
</StyledTableCell>
</StyledTableRow>
));

export default CellTypesTable;
58 changes: 58 additions & 0 deletions context/app/static/js/components/cellTypes/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import useSWR from 'swr';

const fetcher = (url) => fetch(url, { type: 'application/json' }).then((res) => res.json());

export const useCellTypesList = () => {
const { data, error } = useSWR(`/cell-types/list.json`, fetcher, {
revalidateOnFocus: false,
});
return {
cellTypes: data,
isLoading: !error && !data,
isError: error,
};
};

export const useCellTypeDescription = (cellType, shouldFetch) => {
const { data, error } = useSWR(shouldFetch ? `/cell-types/${cellType}/description.json` : null, fetcher, {
revalidateOnFocus: false,
});
return {
description: data,
isLoading: !error && !data,
isError: error,
};
};

export const useCellTypeOrgans = (cellType, shouldFetch) => {
const { data, error } = useSWR(shouldFetch ? `/cell-types/${cellType}/organs.json` : null, fetcher, {
revalidateOnFocus: false,
});
return {
organs: data,
isLoading: !error && !data,
isError: error,
};
};

export const useCellTypeDatasets = (cellType) => {
const { data, error } = useSWR(`/cell-types/${cellType}/datasets.json`, fetcher, {
revalidateOnFocus: false,
});
return {
datasets: data,
isLoading: !error && !data,
isError: error,
};
};

export const useCellTypeAssays = (cellType) => {
const { data, error } = useSWR(`/cell-types/${cellType}/assays.json`, fetcher, {
revalidateOnFocus: false,
});
return {
assays: data,
isLoading: !error && !data,
isError: error,
};
};
Loading