From 55a2bb512806611399aa8ea85e080149489d15cb Mon Sep 17 00:00:00 2001 From: GaelleA Date: Mon, 2 Dec 2024 11:51:44 -0500 Subject: [PATCH] feat(studies): SJIP-1104 add page content --- src/locales/en.ts | 7 + src/services/api/arranger/models.ts | 13 +- src/store/global/thunks.ts | 10 +- src/views/Login/StudiesSection/index.tsx | 8 +- .../components/PageContent/index.module.css | 20 ++ .../components/PageContent/index.tsx | 80 ++++++++ src/views/PublicStudies/index.module.css | 20 ++ src/views/PublicStudies/index.tsx | 32 ++- src/views/PublicStudies/utils.tsx | 189 ++++++++++++++++++ src/views/Studies/index.tsx | 4 +- 10 files changed, 363 insertions(+), 20 deletions(-) create mode 100644 src/views/PublicStudies/components/PageContent/index.module.css create mode 100644 src/views/PublicStudies/components/PageContent/index.tsx create mode 100644 src/views/PublicStudies/index.module.css create mode 100644 src/views/PublicStudies/utils.tsx diff --git a/src/locales/en.ts b/src/locales/en.ts index 045204ee..8bb22fa9 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1440,6 +1440,13 @@ const en = { start: 'Start', title: 'Studies', }, + publicStudies: { + title: 'Studies', + search: { + title: 'Search by study name', + placeholder: 'The Human Trisome Project', + }, + }, analytics: { title: 'Data Analysis', subtitle: 'Quickly visualize and interpret INCLUDE Data with our user-friendly tools.', diff --git a/src/services/api/arranger/models.ts b/src/services/api/arranger/models.ts index 487663c2..0bfb9f82 100644 --- a/src/services/api/arranger/models.ts +++ b/src/services/api/arranger/models.ts @@ -26,8 +26,19 @@ export type Suggestion = { }; export interface IStudiesParticipants { + data_category: string[]; + description?: string; + domains?: string[]; + external_ids?: string[]; + family_count?: number; + file_count?: number; + guid?: string; + is_harmonized?: boolean; participant_count: number; + program: string; study_code: string; + study_id: string; + study_name: string; } export interface IDiagnosis { @@ -47,7 +58,7 @@ export interface IStatistics { samples: number; sex: Record; studies: number; - studiesParticipants: Record; + studiesParticipants: IStudiesParticipants[]; transcriptomes: number; variants: number; } diff --git a/src/store/global/thunks.ts b/src/store/global/thunks.ts index 414d73d6..f0ef7fcc 100644 --- a/src/store/global/thunks.ts +++ b/src/store/global/thunks.ts @@ -10,17 +10,9 @@ const fetchStats = createAsyncThunk { - acc[study_code] = participant_count; - return acc; - }, - {} as Record, - ); - const data: IStatistics = { ...statistics!, - studiesParticipants: formattedStudiesStatistics!, + studiesParticipants: studiesStatistics!, }; return data; diff --git a/src/views/Login/StudiesSection/index.tsx b/src/views/Login/StudiesSection/index.tsx index 16604816..f6036a8e 100644 --- a/src/views/Login/StudiesSection/index.tsx +++ b/src/views/Login/StudiesSection/index.tsx @@ -9,6 +9,7 @@ import BriLogo from 'components/assets/studies/study-logo-BRI.png'; import DefaultLogo from 'components/assets/studies/study-logo-default.svg'; import DsconnectLogo from 'components/assets/studies/study-logo-DSC.png'; import KfLogo from 'components/assets/studies/study-logo-KF.svg'; +import { IStudiesParticipants } from 'services/api/arranger/models'; import { useGlobals } from '../../../store/global'; @@ -35,18 +36,19 @@ const studies = [ { code: 'DS-NEXUS', formattedCode: 'dsnexus', logo: DsnexusLogo }, ]; -const formatStudies = (studiesParticipants: Record) => +const formatStudies = (studiesParticipants: IStudiesParticipants[]) => studies.map((study) => ({ code: study.code, title: Study Logo, subtitle: intl.get(`screen.loginPage.studies.${study.formattedCode}.name`), description: intl.getHTML(`screen.loginPage.studies.${study.formattedCode}.description`), - participantCount: studiesParticipants[study.code], + participantCount: studiesParticipants.find((studyPart) => studyPart.study_code === study.code) + ?.participant_count, })); const StudiesSection = () => { const { stats } = useGlobals(); - const { studiesParticipants = {}, studies: studiesCount = 0 } = stats || {}; + const { studiesParticipants = [], studies: studiesCount = 0 } = stats || {}; const formattedStudies = formatStudies(studiesParticipants); return (
diff --git a/src/views/PublicStudies/components/PageContent/index.module.css b/src/views/PublicStudies/components/PageContent/index.module.css new file mode 100644 index 00000000..ff72848a --- /dev/null +++ b/src/views/PublicStudies/components/PageContent/index.module.css @@ -0,0 +1,20 @@ +.pageContent { + width: 100%; +} +.pageContent .tableWrapper { + width: 100%; +} +.pageContent .title { + margin: 0; +} +.pageContent .label { + margin-bottom: 8px; +} +.pageContent .inputContainer { + display: flex; + gap: 8px; + margin-bottom: 8px; +} +.pageContent .guidButton { + height: auto; +} diff --git a/src/views/PublicStudies/components/PageContent/index.tsx b/src/views/PublicStudies/components/PageContent/index.tsx new file mode 100644 index 00000000..d66c9f63 --- /dev/null +++ b/src/views/PublicStudies/components/PageContent/index.tsx @@ -0,0 +1,80 @@ +import { useState } from 'react'; +import intl from 'react-intl-universal'; +import ProLabel from '@ferlab/ui/core/components/ProLabel'; +import ProTable from '@ferlab/ui/core/components/ProTable'; +import { ProColumnType } from '@ferlab/ui/core/components/ProTable/types'; +import GridCard from '@ferlab/ui/core/view/v2/GridCard'; +import { Input, Space, Typography } from 'antd'; +import { TABLE_ID } from 'views/PublicStudies/utils'; + +import { useGlobals } from 'store/global'; +import { getProTableDictionary } from 'utils/translation'; + +import styles from './index.module.css'; + +const { Title } = Typography; + +type OwnProps = { + defaultColumns: ProColumnType[]; +}; + +const PageContent = ({ defaultColumns = [] }: OwnProps) => { + const [searchValue, setSearchValue] = useState(''); + + const { stats, isFetchingStats } = useGlobals(); + const { studiesParticipants = [] } = stats || {}; + + const searchPrescription = (value: any) => { + if (value?.target?.value) { + setSearchValue(value.target.value); + } else { + setSearchValue(''); + } + }; + + return ( + + + {intl.get('screen.publicStudies.title')} + + +
+ +
+ +
+
+ + ({ ...i, key: i.study_code }))} + dictionary={getProTableDictionary()} + /> + } + /> +
+ ); +}; + +export default PageContent; diff --git a/src/views/PublicStudies/index.module.css b/src/views/PublicStudies/index.module.css new file mode 100644 index 00000000..2f8e00bd --- /dev/null +++ b/src/views/PublicStudies/index.module.css @@ -0,0 +1,20 @@ +.studiesPage { + display: flex; +} + +.scrollContent { + width: 100%; + padding: var(--default-page-content-padding); +} + +.descriptionCell::after { + content: ''; + display: block; +} + +.dbgapLink { + margin-right: 8px; +} +.dbgapLink:last-child { + margin-right: 0; +} diff --git a/src/views/PublicStudies/index.tsx b/src/views/PublicStudies/index.tsx index 7def7415..1fa3e945 100644 --- a/src/views/PublicStudies/index.tsx +++ b/src/views/PublicStudies/index.tsx @@ -1,9 +1,31 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import ScrollContent from '@ferlab/ui/core/layout/ScrollContent'; +import PageContent from 'views/PublicStudies/components/PageContent'; + import PublicLayout from 'components/PublicLayout'; +import { fetchStats } from 'store/global/thunks'; + +import { getColumns, SCROLL_WRAPPER_ID } from './utils'; + +import style from './index.module.css'; + +const PublicStudies = () => { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchStats()); + }, [dispatch]); -const PublicStudies = () => ( - - - -); + return ( + +
+ + + +
+
+ ); +}; export default PublicStudies; diff --git a/src/views/PublicStudies/utils.tsx b/src/views/PublicStudies/utils.tsx new file mode 100644 index 00000000..5527b323 --- /dev/null +++ b/src/views/PublicStudies/utils.tsx @@ -0,0 +1,189 @@ +import intl from 'react-intl-universal'; +import { Link } from 'react-router-dom'; +import { AuditOutlined } from '@ant-design/icons'; +import ExternalLink from '@ferlab/ui/core/components/ExternalLink'; +import { ProColumnType } from '@ferlab/ui/core/components/ProTable/types'; +import ExpandableCell from '@ferlab/ui/core/components/tables/ExpandableCell'; +import { numberWithCommas } from '@ferlab/ui/core/utils/numberUtils'; +import { Space, Tag, Tooltip, Typography } from 'antd'; +import StudyPopoverRedirect from 'views/DataExploration/components/StudyPopoverRedirect'; +import { DataCategory, hasDataCategory } from 'views/Studies'; + +import { TABLE_EMPTY_PLACE_HOLDER } from 'common/constants'; +import { IStudiesParticipants } from 'services/api/arranger/models'; + +import style from './index.module.css'; + +export const SCROLL_WRAPPER_ID = 'public-studies-scroll-wrapper'; +export const TABLE_ID = 'public-studies'; + +export const getColumns = (): ProColumnType[] => [ + { + key: 'is_harmonized', + iconTitle: , + title: intl.get('entities.study.harmonized'), + popoverProps: { + title: {intl.get('screen.studies.harmonizedPopover.title')}, + overlayStyle: { maxWidth: '400px' }, + content: intl.getHTML('screen.studies.harmonizedPopover.content'), + }, + align: 'center', + render: (record: IStudiesParticipants) => { + const harmonizedTag = record.is_harmonized ? ( + + {intl.get('entities.study.harmonizedAbrv')} + + ) : ( + + {intl.get('entities.study.unharmonizedAbrv')} + + ); + const guidTag = record.guid === 'NDAR' && ( + + {intl.get('entities.study.guidAbrv')} + + ); + return ( + + {harmonizedTag} + {guidTag} + + ); + }, + }, + { + key: 'study_code', + title: intl.get('entities.study.code'), + dataIndex: 'study_code', + // TODO Open modal + render: (study_code: string) => {study_code}, + }, + { + key: 'study_name', + title: intl.get('entities.study.name'), + width: 400, + render: (record: IStudiesParticipants) => ( + + ), + }, + { + key: 'program', + title: intl.get('entities.study.program'), + dataIndex: 'program', + render: (program: string) => program || TABLE_EMPTY_PLACE_HOLDER, + }, + { + key: 'domains', + title: intl.get('entities.study.domains'), + dataIndex: 'domains', + render: (domains: string[]) => { + if (!domains || domains.length === 0) { + return TABLE_EMPTY_PLACE_HOLDER; + } + return ( +
{sourceText}
} + /> + ); + }, + width: 300, + }, + { + key: 'external_ids', + title: intl.get('entities.study.dbgap'), + dataIndex: 'external_ids', + render: (external_ids: string[]) => { + if (!external_ids || external_ids.length === 0) { + return TABLE_EMPTY_PLACE_HOLDER; + } + + return ( + ( +
+ + {id} + +
+ )} + /> + ); + }, + }, + { + key: 'description', + title: intl.get('entities.study.description'), + dataIndex: 'description', + render: (description: string) => + description ? ( + + {description} + + ) : ( + TABLE_EMPTY_PLACE_HOLDER + ), + }, + { + key: 'participant_count', + title: intl.get('entities.participant.participants'), + render: (record: IStudiesParticipants) => { + const participantCount = record?.participant_count || 0; + + return participantCount + ? // TODO Open modal + numberWithCommas(participantCount) + : participantCount || TABLE_EMPTY_PLACE_HOLDER; + }, + }, + { + key: 'file_count', + title: intl.get('entities.file.files'), + render: (record: IStudiesParticipants) => { + const fileCount = record?.file_count || 0; + + return fileCount + ? // TODO Open modal + numberWithCommas(fileCount) + : fileCount || TABLE_EMPTY_PLACE_HOLDER; + }, + }, + { + key: 'genomic', + title: intl.get('entities.study.dataCategory.genomic'), + tooltip: intl.get('entities.study.dataCategory.genomicTooltip'), + align: 'center', + render: (record: IStudiesParticipants) => + hasDataCategory(record.data_category, DataCategory.GENOMIC), + }, + { + key: 'transcriptomic', + title: intl.get('entities.study.dataCategory.transcriptomic'), + tooltip: intl.get('entities.study.dataCategory.transcriptomicTooltip'), + align: 'center', + render: (record: IStudiesParticipants) => + hasDataCategory(record.data_category, DataCategory.TRANSCRIPTOMIC), + }, + { + key: 'proteomic', + title: intl.get('entities.study.dataCategory.proteomic'), + tooltip: intl.get('entities.study.dataCategory.proteomicTooltip'), + align: 'center', + render: (record: IStudiesParticipants) => + hasDataCategory(record.data_category, DataCategory.PROTEOMIC), + }, +]; diff --git a/src/views/Studies/index.tsx b/src/views/Studies/index.tsx index 84d0356c..c9dd3163 100644 --- a/src/views/Studies/index.tsx +++ b/src/views/Studies/index.tsx @@ -27,7 +27,7 @@ import { SCROLL_WRAPPER_ID } from './utils/constants'; import styles from './index.module.css'; -const enum DataCategory { +export const enum DataCategory { METABOLOMIC = 'Metabolomics', GENOMIC = 'Genomics', PROTEOMIC = 'Proteomics', @@ -36,7 +36,7 @@ const enum DataCategory { IMMUNE_MAP = 'Immune-Map', } -const hasDataCategory = (dataCategory: string[], category: DataCategory) => +export const hasDataCategory = (dataCategory: string[], category: DataCategory) => dataCategory?.includes(category) ? : TABLE_EMPTY_PLACE_HOLDER; const filterInfo: FilterInfo = {