Skip to content

Commit

Permalink
single dataset metadata drawer (#103)
Browse files Browse the repository at this point in the history
## Description

Implements the metadata drawer for the single dataset page. This PR
introduces the following changes:

- Extract Drawer component and remove global usage in favor of features
using it locally.
- Extract Tabs component for reuse in the metadata drawer.
- Add typed convenience hook for getting the dataset by ID GraphQL
response.
- Refactored drawer state to be consumed through hooks
- Updated GraphQL query to include new properties for the metadata
drawer
- New components:
  - Accordion
  - MetadataTable

## Demos

### Dataset drawer

<img width="1722" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/9e20bad5-61bd-4a4a-be47-65bd6e97daef">

<img width="480" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/bcd52122-eb34-4ee0-bff0-2e6706c6e012">

<img width="484" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/d657dcda-08df-46a7-867a-64b3b1c239dd">

<img width="458" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/8b96ea83-6649-4722-8d02-094938fb8c2d">

### Really long title clamped at 3 lines

<img width="508" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/4d5b380f-43c7-41a3-8cf1-96285b4a88c2">
  • Loading branch information
codemonkey800 authored Oct 31, 2023
1 parent e82082c commit 8bacebd
Show file tree
Hide file tree
Showing 20 changed files with 682 additions and 103 deletions.
33 changes: 33 additions & 0 deletions frontend/packages/data-portal/app/components/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Accordion as SDSAccordion,
AccordionDetails,
AccordionHeader,
} from '@czi-sds/components'
import { ReactNode, useState } from 'react'

export function Accordion({
children,
header,
id,
initialOpen,
}: {
children: ReactNode
header: ReactNode
id: string
initialOpen?: boolean
}) {
const [expanded, setExpanded] = useState(initialOpen)

return (
<SDSAccordion
id={id}
elevation={0}
expanded={expanded}
onChange={(_, nextExpanded) => setExpanded(nextExpanded)}
>
<AccordionHeader>{header}</AccordionHeader>

<AccordionDetails>{children}</AccordionDetails>
</SDSAccordion>
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Tab, Tabs } from '@czi-sds/components'
import { useLoaderData, useLocation, useNavigate } from '@remix-run/react'
import { useMemo } from 'react'

import { GetToolbarDataQuery } from 'app/__generated__/graphql'
import { TabData, Tabs } from 'app/components/Tabs'
import { i18n } from 'app/i18n'

export enum BrowseDataTab {
Expand All @@ -17,28 +18,28 @@ export function BrowseDataTabs() {
: BrowseDataTab.Runs

const data = useLoaderData<GetToolbarDataQuery>()
const datasetCount = data.datasets_aggregate.aggregate?.count ?? 0
const runCount = data.runs_aggregate.aggregate?.count ?? 0

const tabOptions = useMemo<TabData<BrowseDataTab>[]>(
() => [
{
label: i18n.datasetsTab(datasetCount),
value: BrowseDataTab.Datasets,
},
{
label: i18n.runsTab(runCount),
value: BrowseDataTab.Runs,
},
],
[datasetCount, runCount],
)

return (
// TODO fix TypeScript issue upstream. For some reason the Tabs component is
// requiring every prop to be passed rather than making it optional.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<Tabs
sdsSize="large"
onChange={(_, nextTab: BrowseDataTab) =>
navigate(`/browse-data/${nextTab}`)
}
onChange={(nextTab) => navigate(`/browse-data/${nextTab}`)}
value={tab}
>
<Tab
label={i18n.datasetsTab(data.datasets_aggregate.aggregate?.count ?? 0)}
value={BrowseDataTab.Datasets}
/>

<Tab
label={i18n.runsTab(data.runs_aggregate.aggregate?.count ?? 0)}
value={BrowseDataTab.Runs}
/>
</Tabs>
tabs={tabOptions}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentProps } from 'react'

import { Accordion } from 'app/components/Accordion'
import { MetadataTable } from 'app/components/MetadataTable'

type AccordionProps = Pick<ComponentProps<typeof Accordion>, 'id' | 'header'>
type MetadataTableProps = Pick<ComponentProps<typeof MetadataTable>, 'data'>

export function AccordionMetadataTable({
data,
header,
id,
}: AccordionProps & MetadataTableProps) {
return (
<Accordion id={id} header={header} initialOpen>
<MetadataTable
data={data}
tableCellProps={{
loadingSkeleton: false,
}}
/>
</Accordion>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Icon } from '@czi-sds/components'
import { useSearchParams } from '@remix-run/react'

import { Link } from 'app/components/Link'
import { useDatasetById } from 'app/hooks/useDatasetById'
import { i18n } from 'app/i18n'
import { cns } from 'app/utils/cns'

export function DatasetHeader() {
const [params] = useSearchParams()
const previousUrl = params.get('prev')
const dataset = useDatasetById()

return (
<header className="flex flex-col items-center justify-center w-full min-h-[48px]">
<div
className={cns(
'flex items-center',
'px-sds-xl py-sds-l',
'w-full max-w-content',
previousUrl ? 'justify-between' : 'justify-end',
)}
>
{previousUrl && (
<Link className="flex items-center gap-1" to={previousUrl}>
<Icon
sdsIcon="chevronLeft"
sdsSize="xs"
sdsType="iconButton"
className="!w-[10px] !h-[10px] !fill-sds-primary-400"
/>
<span className="text-sds-primary-400 font-semibold text-sm">
Back to results
</span>
</Link>
)}

<div className="flex items-center gap-sds-xs text-xs text-sds-gray-600">
<p>{i18n.releaseDate(dataset.release_date)}</p>
<div className="h-3 w-px bg-sds-gray-400" />
<p>
{i18n.lastModified(
dataset.last_modified_date ?? dataset.deposition_date,
)}
</p>
</div>
</div>
</header>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { ButtonIcon } from '@czi-sds/components'
import { useSearchParams } from '@remix-run/react'
import { useEffect, useRef } from 'react'

import { Demo } from 'app/components/Demo'
import { Drawer } from 'app/components/Drawer'
import { TabData, Tabs } from 'app/components/Tabs'
import { useDatasetById } from 'app/hooks/useDatasetById'
import { i18n } from 'app/i18n'
import { useDatasetDrawer } from 'app/state/drawer'
import { cns } from 'app/utils/cns'

import { DatasetMetadataTable } from './DatasetMetadataTable'
import { SampleAndExperimentConditionsTable } from './SampleAndExperimentConditionsTable'
import { TiltSeriesTable } from './TiltSeriesTable'

enum MetadataTab {
Metadata = 'metadata',
HowToCite = 'howToCite',
}

const TAB_OPTIONS: TabData<MetadataTab>[] = [
{
label: i18n.metadata,
value: MetadataTab.Metadata,
},
{
label: i18n.howToCite,
value: MetadataTab.HowToCite,
},
]

const ACTIVE_TAB_PARAM = 'tab'

export function DatasetMetadataDrawer() {
const drawer = useDatasetDrawer()
const dataset = useDatasetById()

const [searchParams, setSearchParams] = useSearchParams()
const activeTab = (searchParams.get(ACTIVE_TAB_PARAM) ??
MetadataTab.Metadata) as MetadataTab

const initialLoadRef = useRef(true)
if (initialLoadRef.current && searchParams.has(ACTIVE_TAB_PARAM)) {
initialLoadRef.current = false
drawer.setOpen(true)
}

useEffect(() => {
if (drawer.open && searchParams.get(ACTIVE_TAB_PARAM) !== activeTab) {
setSearchParams((params) => {
params.set(ACTIVE_TAB_PARAM, activeTab)
return params
})
} else if (!drawer.open) {
setSearchParams((params) => {
params.delete(ACTIVE_TAB_PARAM)
return params
})
}
}, [activeTab, drawer.open, searchParams, setSearchParams])

return (
<Drawer open={drawer.open} onClose={() => drawer.setOpen(false)}>
<div className="flex flex-col flex-auto">
<header className="flex items-start justify-between px-sds-xl pt-sds-xl pb-sds-xxl">
<div className="flex flex-col gap-sds-s">
<p className="text-xs text-sds-gray-600 font-semibold uppercase">
{i18n.datasetDetails}
</p>

<p className="text-sds-header-xl font-semibold text-black leading-[30px] line-clamp-3">
{dataset.title}
</p>
</div>

<ButtonIcon
onClick={() => drawer.setOpen(false)}
sdsIcon="xMark"
sdsIconProps={{
color: 'gray',
}}
/>
</header>

<div className="px-sds-xl border-b-2 border-sds-gray-200">
<Tabs
className="!m-0"
tabs={TAB_OPTIONS}
value={activeTab}
onChange={(tab) =>
setSearchParams((params) => {
params.set(ACTIVE_TAB_PARAM, tab)
return params
})
}
/>
</div>

<div
className={cns(
'flex flex-col flex-auto',
'px-sds-xl pt-sds-xl pb-sds-xxl',

activeTab === MetadataTab.Metadata &&
'divide-y divide-sds-gray-300',
)}
>
{activeTab === MetadataTab.Metadata && (
<>
<DatasetMetadataTable />
<SampleAndExperimentConditionsTable />
<TiltSeriesTable />
</>
)}

{activeTab === MetadataTab.HowToCite && <Demo>How to cite</Demo>}
</div>
</div>
</Drawer>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useDatasetById } from 'app/hooks/useDatasetById'
import { i18n } from 'app/i18n'

import { AccordionMetadataTable } from './AccordionMetadataTable'
import { getTableData } from './utils'

export function DatasetMetadataTable() {
const dataset = useDatasetById()
const datasetMetadata = getTableData(
{
label: i18n.depositionDate,
values: [dataset.deposition_date],
},
{
label: i18n.affiliationName,
values: dataset.authors
.map((author) => author.affiliation_name)
.filter((value): value is string => !!value),
},
{
label: i18n.fundingAgency,
values: dataset.funding_sources.map(
(source) => source.funding_agency_name,
),
},
{
label: i18n.relatedDatabases,
values: dataset.funding_sources.map(
(source) => source.funding_agency_name,
),
},
{
label: i18n.relatedDatabases,
// TODO implement when data is available
values: ['TBD'],
},
{
label: i18n.citations,
// TODO implement when data is available
values: ['TBD'],
},
)

return (
<AccordionMetadataTable
id="dataset-metadata"
header={i18n.datasetMetadata}
data={datasetMetadata}
/>
)
}
Loading

0 comments on commit 8bacebd

Please sign in to comment.