diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.test.tsx b/frontend/packages/data-portal/app/components/AuthorList/AuthorList.test.tsx
similarity index 86%
rename from frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.test.tsx
rename to frontend/packages/data-portal/app/components/AuthorList/AuthorList.test.tsx
index 8e5b019b2..56e9ce1b1 100644
--- a/frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.test.tsx
+++ b/frontend/packages/data-portal/app/components/AuthorList/AuthorList.test.tsx
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'
import { AuthorInfo, MockAuthorLink } from 'app/components/AuthorLink'
-import { DatasetAuthors } from './DatasetAuthors'
+import { AuthorList } from './AuthorList'
const DEFAULT_AUTHORS: AuthorInfo[] = [
{ name: 'Foo', corresponding_author_status: true },
@@ -19,7 +19,7 @@ const AUTHOR_MAP = Object.fromEntries(
)
it('should render authors', () => {
- render()
+ render()
DEFAULT_AUTHORS.forEach((author) =>
expect(screen.getByText(author.name)).toBeInTheDocument(),
@@ -27,7 +27,7 @@ it('should render authors', () => {
})
it('should sort primary authors', () => {
- render()
+ render()
const authorNode = screen.getByRole('paragraph')
const authors = (authorNode.textContent ?? '').split(', ')
@@ -36,7 +36,7 @@ it('should sort primary authors', () => {
})
it('should sort other authors', () => {
- render()
+ render()
const authorNode = screen.getByRole('paragraph')
const authors = (authorNode.textContent ?? '').split(', ')
const otherAuthors = authors.slice(2, -2)
@@ -48,7 +48,7 @@ it('should sort other authors', () => {
})
it('should sort corresponding authors', () => {
- render()
+ render()
const authorNode = screen.getByRole('paragraph')
const authors = (authorNode.textContent ?? '').split(', ')
@@ -66,9 +66,7 @@ it('should render author links', () => {
orcid: `0000-0000-0000-000${idx}`,
}))
- render(
- ,
- )
+ render()
authors.forEach((author) =>
expect(
@@ -84,7 +82,7 @@ it('should not render author links when compact', () => {
}))
render(
- {
})
it('should not render other authors when compact', () => {
- render()
+ render()
const authorNode = screen.getByRole('paragraph')
const authors = (authorNode.textContent ?? '').split(', ')
const otherAuthors = authors.slice(2, -2)
@@ -106,13 +104,13 @@ it('should not render other authors when compact', () => {
})
it('should render comma if compact and has corresponding authors', () => {
- render()
+ render()
expect(screen.getByText((text) => text.includes('... ,'))).toBeInTheDocument()
})
it('should not render comma for others if compact and no corresponding authors', () => {
render(
- !author.corresponding_author_status,
)}
diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.tsx b/frontend/packages/data-portal/app/components/AuthorList/AuthorList.tsx
similarity index 98%
rename from frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.tsx
rename to frontend/packages/data-portal/app/components/AuthorList/AuthorList.tsx
index 9eb2e29f2..67dc88cbe 100644
--- a/frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.tsx
+++ b/frontend/packages/data-portal/app/components/AuthorList/AuthorList.tsx
@@ -13,7 +13,7 @@ function getAuthorIds(authors: AuthorInfo[]) {
return authors.map((author) => author.name + author.email + author.orcid)
}
-export function DatasetAuthors({
+export function AuthorList({
AuthorLinkComponent = AuthorLink,
authors,
className,
diff --git a/frontend/packages/data-portal/app/components/AuthorList/index.ts b/frontend/packages/data-portal/app/components/AuthorList/index.ts
new file mode 100644
index 000000000..f5ab0146a
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/AuthorList/index.ts
@@ -0,0 +1 @@
+export * from './AuthorList'
diff --git a/frontend/packages/data-portal/app/components/Breadcrumbs.tsx b/frontend/packages/data-portal/app/components/Breadcrumbs.tsx
index 96f9b58e3..530bef698 100644
--- a/frontend/packages/data-portal/app/components/Breadcrumbs.tsx
+++ b/frontend/packages/data-portal/app/components/Breadcrumbs.tsx
@@ -40,10 +40,10 @@ function Breadcrumb({
export function Breadcrumbs({
variant,
- dataset,
+ data,
}: {
- variant: 'dataset' | 'run'
- dataset: { id: number; title: string }
+ variant: 'dataset' | 'deposition' | 'run'
+ data: { id: number; title: string }
}) {
const { t } = useI18n()
@@ -51,26 +51,28 @@ export function Breadcrumbs({
const { singleDatasetHistory } = useSingleDatasetFilterHistory()
const browseAllLink = useMemo(() => {
- const url = '/browse-data/datasets'
- const encodedParams = encodeParams(
- Array.from(browseDatasetHistory?.entries() ?? []),
- )
+ const url =
+ variant === 'deposition'
+ ? '/browse-data/depositions'
+ : '/browse-data/datasets'
+ const history = variant === 'deposition' ? undefined : browseDatasetHistory
+ const encodedParams = encodeParams(Array.from(history?.entries() ?? []))
return `${url}?${encodedParams}`
- }, [browseDatasetHistory])
+ }, [browseDatasetHistory, variant])
const singleDatasetLink = useMemo(() => {
if (variant === 'dataset') {
return undefined
}
- const url = `/datasets/${dataset.id}`
+ const url = `/datasets/${data.id}`
const encodedParams = encodeParams(
Array.from(singleDatasetHistory?.entries() ?? []),
)
return `${url}?${encodedParams}`
- }, [singleDatasetHistory, variant, dataset])
+ }, [singleDatasetHistory, variant, data])
const chevronIcon = (
@@ -79,20 +81,24 @@ export function Breadcrumbs({
return (
{chevronIcon}
-
+ {variant === 'deposition' ? (
+
+ ) : (
+
+ )}
{variant === 'run' && (
<>
{chevronIcon}
diff --git a/frontend/packages/data-portal/app/components/BrowseData/DatasetTable.tsx b/frontend/packages/data-portal/app/components/BrowseData/DatasetTable.tsx
index c9722c24f..d4a0da942 100644
--- a/frontend/packages/data-portal/app/components/BrowseData/DatasetTable.tsx
+++ b/frontend/packages/data-portal/app/components/BrowseData/DatasetTable.tsx
@@ -8,7 +8,7 @@ import { range } from 'lodash-es'
import { useEffect, useMemo, useState } from 'react'
import { AnnotatedObjectsList } from 'app/components/AnnotatedObjectsList'
-import { DatasetAuthors } from 'app/components/Dataset/DatasetAuthors'
+import { AuthorList } from 'app/components/AuthorList'
import { I18n } from 'app/components/I18n'
import { KeyPhoto } from 'app/components/KeyPhoto'
import { Link } from 'app/components/Link'
@@ -170,7 +170,7 @@ export function DatasetTable() {
/>
>
) : (
-
+
)}
diff --git a/frontend/packages/data-portal/app/components/BrowseData/DepositionTable.tsx b/frontend/packages/data-portal/app/components/BrowseData/DepositionTable.tsx
index c686c6b85..278d0ab60 100644
--- a/frontend/packages/data-portal/app/components/BrowseData/DepositionTable.tsx
+++ b/frontend/packages/data-portal/app/components/BrowseData/DepositionTable.tsx
@@ -8,7 +8,7 @@ import { range, sum } from 'lodash-es'
import { useMemo } from 'react'
import { AnnotatedObjectsList } from 'app/components/AnnotatedObjectsList'
-import { DatasetAuthors } from 'app/components/Dataset/DatasetAuthors'
+import { AuthorList } from 'app/components/AuthorList'
import { KeyPhoto } from 'app/components/KeyPhoto'
import { Link } from 'app/components/Link'
import { CellHeader, PageTable, TableCell } from 'app/components/Table'
@@ -147,7 +147,7 @@ export function DepositionTable() {
/>
>
) : (
-
+
)}
diff --git a/frontend/packages/data-portal/app/components/CollapsibleList.tsx b/frontend/packages/data-portal/app/components/CollapsibleList.tsx
new file mode 100644
index 000000000..37470dfde
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/CollapsibleList.tsx
@@ -0,0 +1,89 @@
+import { Icon } from '@czi-sds/components'
+import { ReactNode, useState } from 'react'
+
+import { useI18n } from 'app/hooks/useI18n'
+import { cns } from 'app/utils/cns'
+
+interface ListEntry {
+ key: string
+ entry: ReactNode
+}
+
+export function CollapsibleList({
+ entries,
+ collapseAfter,
+ tableVariant = false,
+}: {
+ entries?: ListEntry[]
+ collapseAfter?: number
+ tableVariant?: boolean
+}) {
+ const collapsible =
+ collapseAfter !== undefined &&
+ collapseAfter >= 0 &&
+ entries !== undefined &&
+ entries.length > collapseAfter + 1
+
+ const { t } = useI18n()
+ const [collapsed, setCollapsed] = useState(true)
+
+ return entries ? (
+
+ {entries.map(
+ ({ key, entry }, i) =>
+ !(collapsible && collapsed && i + 1 > collapseAfter) && (
+ - {entry}
+ ),
+ )}
+ {collapsible && (
+
+
+
+ )}
+
+ ) : (
+
+ {t('notSubmitted')}
+
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/DatabaseList.tsx b/frontend/packages/data-portal/app/components/DatabaseList.tsx
new file mode 100644
index 000000000..1491e19da
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/DatabaseList.tsx
@@ -0,0 +1,20 @@
+import { CollapsibleList } from 'app/components/CollapsibleList'
+import { DatabaseEntry } from 'app/components/DatabaseEntry'
+
+export function DatabaseList({
+ entries,
+ collapseAfter,
+}: {
+ entries?: string[]
+ collapseAfter?: number
+}) {
+ return (
+ ({
+ key: e,
+ entry: ,
+ }))}
+ collapseAfter={collapseAfter}
+ />
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetDescription.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetDescription.tsx
deleted file mode 100644
index baa627abe..000000000
--- a/frontend/packages/data-portal/app/components/Dataset/DatasetDescription.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import { Button } from '@czi-sds/components'
-import { useState } from 'react'
-
-import { AuthorLegend } from 'app/components/AuthorLegend'
-import { DatabaseEntry } from 'app/components/DatabaseEntry'
-import { DOI_ID } from 'app/constants/external-dbs'
-import { useDatasetById } from 'app/hooks/useDatasetById'
-import { i18n } from 'app/i18n'
-import { cns, cnsNoMerge } from 'app/utils/cns'
-
-import { DatasetAuthors } from './DatasetAuthors'
-
-// use clsx here instead of cns since it erroneously merges text-sds-gray-500 and text-sds-caps-xxxs
-const sectionHeaderStyles = cnsNoMerge(
- 'font-semibold uppercase',
- 'text-sds-gray-black',
- 'text-sds-caps-xxxs leading-sds-caps-xxxs tracking-sds-caps',
-)
-
-interface DatabaseListProps {
- title: string
- entries?: string[]
- className?: string
- collapseAfter?: number
-}
-
-function DatabaseList(props: DatabaseListProps) {
- const { title, entries, className, collapseAfter } = props
- const collapsible =
- collapseAfter !== undefined &&
- collapseAfter >= 0 &&
- entries !== undefined &&
- entries.length > collapseAfter
- const [collapsed, setCollapsed] = useState(true)
-
- return (
-
-
{title}
- {entries ? (
-
- {entries.map(
- (e, i) =>
- !(collapsible && collapsed && i + 1 > collapseAfter) && (
- -
-
-
- ),
- )}
- {collapsible && (
-
-
-
- )}
-
- ) : (
-
- {i18n.notSubmitted}
-
- )}
-
- )
-}
-
-export function DatasetDescription() {
- const { dataset } = useDatasetById()
-
- // clean up entries into lists
- const publicationEntries = dataset.dataset_publications
- ?.split(',')
- .map((e) => e.trim())
- .filter((e) => DOI_ID.exec(e)) // only show DOI links
-
- const relatedDatabaseEntries = dataset.related_database_entries
- ?.split(',')
- .map((e) => e.trim())
-
- return (
-
-
- {dataset.description}
-
-
-
-
-
- {/* extra div to turn it into 3 columns */}
-
-
-
- )
-}
diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetHeader.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetHeader.tsx
index 07163e952..1efe1a638 100644
--- a/frontend/packages/data-portal/app/components/Dataset/DatasetHeader.tsx
+++ b/frontend/packages/data-portal/app/components/Dataset/DatasetHeader.tsx
@@ -1,10 +1,9 @@
import { Button, Icon } from '@czi-sds/components'
import { Breadcrumbs } from 'app/components/Breadcrumbs'
-import { DatasetDescription } from 'app/components/Dataset/DatasetDescription'
+import { DatasetOverview } from 'app/components/Dataset/DatasetOverview'
import { KeyPhoto } from 'app/components/KeyPhoto'
import { PageHeader } from 'app/components/PageHeader'
-import { PageHeaderSubtitle } from 'app/components/PageHeaderSubtitle'
import { useDatasetById } from 'app/hooks/useDatasetById'
import { useDownloadModalQueryParamState } from 'app/hooks/useDownloadModalQueryParamState'
import { useI18n } from 'app/hooks/useI18n'
@@ -31,7 +30,7 @@ export function DatasetHeader() {
{t('downloadDataset')}
}
- breadcrumbs={}
+ breadcrumbs={}
lastModifiedDate={dataset.last_modified_date ?? dataset.deposition_date}
metadata={[
{
@@ -52,9 +51,7 @@ export function DatasetHeader() {
-
{t('datasetOverview')}
-
-
+
{moreInfo}
diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataTable.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataTable.tsx
index 13c4ea4a2..0c3d16f91 100644
--- a/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataTable.tsx
+++ b/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataTable.tsx
@@ -4,12 +4,12 @@ import { isString } from 'lodash-es'
import { AccordionMetadataTable } from 'app/components/AccordionMetadataTable'
import { AuthorLegend } from 'app/components/AuthorLegend'
import { AuthorInfo } from 'app/components/AuthorLink'
+import { AuthorList } from 'app/components/AuthorList'
import { DatabaseEntry } from 'app/components/DatabaseEntry'
import { Link } from 'app/components/Link'
import { useI18n } from 'app/hooks/useI18n'
import { getTableData } from 'app/utils/table'
-import { DatasetAuthors } from './DatasetAuthors'
import { DatasetType } from './type'
function DatabaseEntryList({ entries }: { entries: string }) {
@@ -95,9 +95,7 @@ export function DatasetMetadataTable({
: t('authors'),
labelExtra: ,
renderValue: () => {
- return (
-
- )
+ return
},
values: [],
className: 'leading-sds-body-s',
diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetOverview.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetOverview.tsx
new file mode 100644
index 000000000..5173ccf89
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/Dataset/DatasetOverview.tsx
@@ -0,0 +1,72 @@
+import { AuthorLegend } from 'app/components/AuthorLegend'
+import { AuthorList } from 'app/components/AuthorList'
+import { DatabaseList } from 'app/components/DatabaseList'
+import { DOI_ID } from 'app/constants/external-dbs'
+import { useDatasetById } from 'app/hooks/useDatasetById'
+import { useI18n } from 'app/hooks/useI18n'
+import { cnsNoMerge } from 'app/utils/cns'
+
+import { PageHeaderSubtitle } from '../PageHeaderSubtitle'
+
+// use clsx here instead of cns since it erroneously merges text-sds-gray-500 and text-sds-caps-xxxs
+const sectionHeaderStyles = cnsNoMerge(
+ 'font-semibold uppercase',
+ 'text-sds-gray-black',
+ 'text-sds-caps-xxxs leading-sds-caps-xxxs tracking-sds-caps',
+ 'mb-sds-xs',
+)
+
+export function DatasetOverview() {
+ const { dataset } = useDatasetById()
+ const { t } = useI18n()
+
+ // clean up entries into lists
+ const publicationEntries = dataset.dataset_publications
+ ?.split(',')
+ .map((e) => e.trim())
+ .filter((e) => DOI_ID.exec(e)) // only show DOI links
+
+ const relatedDatabaseEntries = dataset.related_database_entries
+ ?.split(',')
+ .map((e) => e.trim())
+
+ return (
+
+
+ {t('datasetOverview')}
+
+
+ {dataset.description}
+
+
+
+
+
{t('publications')}
+
+
+
+
{t('relatedDatabases')}
+
+
+
+ {/* extra div to turn it into 3 columns */}
+
+
+
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/Deposition/AnnotationMethodsSummary.tsx b/frontend/packages/data-portal/app/components/Deposition/AnnotationMethodsSummary.tsx
new file mode 100644
index 000000000..e5e532f49
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/Deposition/AnnotationMethodsSummary.tsx
@@ -0,0 +1,238 @@
+import { Icon } from '@czi-sds/components'
+import { ReactNode } from 'react'
+
+import { CollapsibleList } from 'app/components/CollapsibleList'
+import { I18n } from 'app/components/I18n'
+import { SourceCodeIcon, WeightsIcon } from 'app/components/icons'
+import { Link } from 'app/components/Link'
+import { PageHeaderSubtitle } from 'app/components/PageHeaderSubtitle'
+import { Tooltip } from 'app/components/Tooltip'
+import {
+ methodLabels,
+ methodTooltipLabels,
+ MethodType,
+} from 'app/constants/methodTypes'
+import { useI18n } from 'app/hooks/useI18n'
+import { I18nKeys } from 'app/types/i18n'
+
+interface MethodLink {
+ i18nLabel: I18nKeys
+ url: string
+ icon: ReactNode
+ title?: string
+}
+
+const iconMap = {
+ sourceCode: (
+
+ ),
+ modelWeights: (
+
+ ),
+ website: (
+
+ ),
+ documentation: (
+
+ ),
+ other: (
+
+ ),
+} as const
+
+interface MethodLinkVariantProps {
+ variant: keyof typeof iconMap
+ url: string
+ title?: string
+}
+
+const variantOrder: (keyof typeof iconMap)[] = [
+ 'sourceCode',
+ 'modelWeights',
+ 'website',
+ 'documentation',
+ 'other',
+]
+
+function methodLinkFromVariant({
+ variant,
+ url,
+ title,
+}: MethodLinkVariantProps): MethodLink {
+ return {
+ i18nLabel: variant,
+ url,
+ title,
+ icon: iconMap[variant],
+ }
+}
+
+function generateMethodLinks(links: MethodLinkVariantProps[]): MethodLink[] {
+ return links
+ .toSorted(
+ (a, b) =>
+ variantOrder.indexOf(a.variant) - variantOrder.indexOf(b.variant),
+ )
+ .map((props) => methodLinkFromVariant(props))
+}
+
+function MethodTypeSection({
+ methodType,
+ links,
+}: {
+ methodType: MethodType
+ links?: MethodLink[]
+}) {
+ const { t } = useI18n()
+
+ return (
+
+
+ {t('methodType')}
+
+
+ {t(methodLabels[methodType])}
+ }
+ >
+
+
+
+
+ {t('methodLinks')}
+
+
({
+ key: `${link.url}_${link.i18nLabel}_${link.title}`,
+ entry: (
+
+
+ {link.icon}
+
+ {t(link.i18nLabel)}:
+
+
+
+ {link.title ?? link.url}
+
+
+ ),
+ }))}
+ collapseAfter={1}
+ />
+
+ )
+}
+
+export function AnnotationMethodsSummary() {
+ const { t } = useI18n()
+
+ const separator =
+
+ const hybridMethodLinks: MethodLinkVariantProps[] = [
+ {
+ variant: 'sourceCode',
+ url: 'https://www.example.com',
+ },
+ {
+ variant: 'website',
+ url: 'https://www.example.com',
+ title: 'Optional Custom Link Name',
+ },
+ ]
+
+ const automatedMethodLinks: MethodLinkVariantProps[] = [
+ {
+ variant: 'sourceCode',
+ url: 'https://www.example.com',
+ },
+ {
+ variant: 'website',
+ url: 'https://www.example.com',
+ title: 'Optional Custom Link Name',
+ },
+ {
+ variant: 'website',
+ url: 'https://www.example.com',
+ },
+ {
+ variant: 'sourceCode',
+ url: 'https://www.example.com',
+ title: 'Optional Custom Link Name',
+ },
+ {
+ variant: 'other',
+ url: 'https://www.example.com',
+ },
+ {
+ variant: 'documentation',
+ url: 'https://www.example.com',
+ title: 'Optional Custom Link Name',
+ },
+ {
+ variant: 'modelWeights',
+ url: 'https://www.example.com',
+ },
+ {
+ variant: 'other',
+ url: 'https://www.example.com',
+ title: 'Optional Custom Link Name',
+ },
+ {
+ variant: 'documentation',
+ url: 'https://www.example.com',
+ },
+ {
+ variant: 'modelWeights',
+ url: 'https://www.example.com',
+ title: 'Optional Custom Link Name',
+ },
+ ]
+
+ return (
+
+
+ {t('annotationMethodsSummary')}
+
+
+
+ {separator}
+
+ {separator}
+
+
+
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/Deposition/DatasetsTable.tsx b/frontend/packages/data-portal/app/components/Deposition/DatasetsTable.tsx
new file mode 100644
index 000000000..bba62b91f
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/Deposition/DatasetsTable.tsx
@@ -0,0 +1,307 @@
+/* eslint-disable react/no-unstable-nested-components */
+
+import { CellHeaderDirection } from '@czi-sds/components'
+import Skeleton from '@mui/material/Skeleton'
+import { useSearchParams } from '@remix-run/react'
+import { ColumnDef, createColumnHelper } from '@tanstack/react-table'
+import { range, sum } from 'lodash-es'
+import { useMemo } from 'react'
+
+import { AnnotatedObjectsList } from 'app/components/AnnotatedObjectsList'
+import { AuthorList } from 'app/components/AuthorList'
+import { I18n } from 'app/components/I18n'
+import { KeyPhoto } from 'app/components/KeyPhoto'
+import { Link } from 'app/components/Link'
+import { CellHeader, PageTable, TableCell } from 'app/components/Table'
+import { ANNOTATED_OBJECTS_MAX, MAX_PER_PAGE } from 'app/constants/pagination'
+import { DepositionPageDatasetTableWidths } from 'app/constants/table'
+import { Dataset, useDepositionById } from 'app/hooks/useDepositionById'
+import { useI18n } from 'app/hooks/useI18n'
+import { useIsLoading } from 'app/hooks/useIsLoading'
+import { LogLevel } from 'app/types/logging'
+import { cnsNoMerge } from 'app/utils/cns'
+import { sendLogs } from 'app/utils/logging'
+import { getErrorMessage } from 'app/utils/string'
+
+const LOADING_DATASETS = range(0, MAX_PER_PAGE).map(
+ (value) =>
+ ({
+ authors: [],
+ id: value,
+ title: `loading-dataset-${value}`,
+ runs: [],
+ runs_aggregate: {},
+ }) as Dataset,
+)
+
+export function DatasetsTable() {
+ const { t } = useI18n()
+ const { deposition } = useDepositionById()
+
+ const [searchParams, setSearchParams] = useSearchParams()
+ const datasetSort = (searchParams.get('sort') ?? undefined) as
+ | CellHeaderDirection
+ | undefined
+
+ const { isLoadingDebounced } = useIsLoading()
+
+ const columns = useMemo(() => {
+ const columnHelper = createColumnHelper()
+
+ try {
+ return [
+ columnHelper.accessor('key_photo_thumbnail_url', {
+ header: () => ,
+
+ cell({ row: { original: dataset } }) {
+ const datasetUrl = `/datasets/${dataset.id}`
+
+ return (
+
+
+
+
+
+ )
+ },
+ }),
+
+ columnHelper.accessor('id', {
+ header: () => (
+ {
+ event.stopPropagation()
+ event.preventDefault()
+ const nextParams = new URLSearchParams(searchParams)
+
+ if (datasetSort === undefined) {
+ nextParams.set('sort', 'asc')
+ } else if (datasetSort === 'asc') {
+ nextParams.set('sort', 'desc')
+ } else {
+ nextParams.delete('sort')
+ }
+
+ setSearchParams(nextParams)
+ }}
+ width={DepositionPageDatasetTableWidths.id}
+ >
+ {t('datasetName')}
+
+ ),
+
+ cell({ row: { original: dataset } }) {
+ const datasetUrl = `/datasets/${dataset.id}`
+
+ return (
+
+
+
+ {isLoadingDebounced ? (
+
+ ) : (
+ {dataset.title}
+ )}
+
+
+
+ {isLoadingDebounced ? (
+
+ ) : (
+ `${t('datasetId')}: ${dataset.id}`
+ )}
+
+
+
+ {isLoadingDebounced ? (
+ <>
+
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+ )
+ },
+ }),
+
+ columnHelper.accessor('organism_name', {
+ header: () => (
+
+ {t('organism')}
+
+ ),
+
+ cell: ({ getValue }) => (
+
+ ),
+ }),
+
+ columnHelper.accessor(
+ (dataset) => dataset.runs_aggregate.aggregate?.count,
+ {
+ id: 'runs',
+
+ header: () => (
+
+
+
+ {t('symbolPeriod')}
+
+
+
+
+
+ }
+ arrowPadding={{ right: 270 }}
+ width={DepositionPageDatasetTableWidths.runs}
+ subHeader={t('withDepositionData')}
+ >
+ {t('runs')}
+
+ ),
+
+ cell: ({ getValue }) => (
+
+ ),
+ },
+ ),
+
+ columnHelper.accessor((dataset) => dataset.runs, {
+ id: 'annotations',
+
+ header: () => (
+
+ {t('annotations')}
+
+ ),
+
+ cell({ getValue }) {
+ const runs = getValue()
+ const annotationCount = sum(
+ runs.flatMap((run) =>
+ run.tomogram_voxel_spacings.flatMap(
+ (voxelSpacing) =>
+ voxelSpacing.annotations_aggregate.aggregate?.count ?? 0,
+ ),
+ ),
+ )
+
+ return (
+
+ {annotationCount.toLocaleString()}
+
+ )
+ },
+ }),
+
+ columnHelper.accessor((dataset) => dataset.runs, {
+ id: 'annotatedObjects',
+
+ header: () => (
+
+ {t('annotatedObjects')}
+
+ ),
+
+ cell({ getValue }) {
+ const runs = getValue()
+ const annotatedObjects = Array.from(
+ new Set(
+ runs.flatMap((run) =>
+ run.tomogram_voxel_spacings.flatMap((voxelSpacing) =>
+ voxelSpacing.annotations.flatMap(
+ (annotation) => annotation.object_name,
+ ),
+ ),
+ ),
+ ),
+ )
+
+ return (
+ (
+
+ {range(0, ANNOTATED_OBJECTS_MAX).map((val) => (
+
+ ))}
+
+ )}
+ >
+ {annotatedObjects.length === 0 ? (
+ '--'
+ ) : (
+
+ )}
+
+ )
+ },
+ }),
+ ] as ColumnDef[]
+ } catch (err) {
+ sendLogs({
+ level: LogLevel.Error,
+ messages: [
+ {
+ type: 'browser',
+ message: 'Error creating columns for dataset table',
+ error: getErrorMessage(err),
+ },
+ ],
+ })
+
+ throw err
+ }
+ }, [datasetSort, isLoadingDebounced, searchParams, setSearchParams, t])
+
+ return (
+
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/Deposition/DepositionHeader.tsx b/frontend/packages/data-portal/app/components/Deposition/DepositionHeader.tsx
new file mode 100644
index 000000000..f3935444d
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/Deposition/DepositionHeader.tsx
@@ -0,0 +1,51 @@
+import { Breadcrumbs } from 'app/components/Breadcrumbs'
+import { KeyPhoto } from 'app/components/KeyPhoto'
+import { PageHeader } from 'app/components/PageHeader'
+import { useDepositionById } from 'app/hooks/useDepositionById'
+import { useI18n } from 'app/hooks/useI18n'
+import {
+ MetadataDrawerId,
+ useMetadataDrawer,
+} from 'app/hooks/useMetadataDrawer'
+
+import { DepositionOverview } from './DepositionOverview'
+
+export function DepositionHeader() {
+ const { deposition } = useDepositionById()
+ const { toggleDrawer } = useMetadataDrawer()
+ const { t } = useI18n()
+
+ return (
+ }
+ lastModifiedDate={
+ deposition.last_modified_date ?? deposition.deposition_date
+ }
+ metadata={[
+ {
+ key: t('depositionId'),
+ value: String(deposition.id),
+ },
+ ]}
+ onMoreInfoClick={() => toggleDrawer(MetadataDrawerId.Deposition)}
+ releaseDate={deposition.release_date}
+ title={deposition.title}
+ renderHeader={({ moreInfo }) => (
+
+
+
+
+
+
+
+
+ {moreInfo}
+
+
+ )}
+ />
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/Deposition/DepositionOverview.tsx b/frontend/packages/data-portal/app/components/Deposition/DepositionOverview.tsx
new file mode 100644
index 000000000..ffc762f64
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/Deposition/DepositionOverview.tsx
@@ -0,0 +1,102 @@
+import { sum } from 'lodash-es'
+import { useMemo } from 'react'
+
+import { AuthorLegend } from 'app/components/AuthorLegend'
+import { AuthorList } from 'app/components/AuthorList'
+import { DatabaseList } from 'app/components/DatabaseList'
+import { PageHeaderSubtitle } from 'app/components/PageHeaderSubtitle'
+import { DOI_ID } from 'app/constants/external-dbs'
+import { useDepositionById } from 'app/hooks/useDepositionById'
+import { useI18n } from 'app/hooks/useI18n'
+import { cnsNoMerge } from 'app/utils/cns'
+
+import { AnnotationMethodsSummary } from './AnnotationMethodsSummary'
+
+// use clsx here instead of cns since it erroneously merges text-sds-gray-500 and text-sds-caps-xxxs
+const sectionHeaderStyles = cnsNoMerge(
+ 'font-semibold uppercase',
+ 'text-sds-gray-black',
+ 'text-sds-caps-xxxs leading-sds-caps-xxxs tracking-sds-caps',
+ 'mb-sds-xs',
+)
+
+export function DepositionOverview() {
+ const { deposition } = useDepositionById()
+
+ const { t } = useI18n()
+
+ const annotationsCount = useMemo(
+ () =>
+ sum(
+ deposition.datasets.flatMap((dataset) =>
+ dataset.runs.flatMap(
+ (run) =>
+ run.tomogram_voxel_spacings.at(0)?.annotations_aggregate.aggregate
+ ?.count,
+ ),
+ ),
+ ),
+ [deposition],
+ )
+
+ // clean up entries into lists
+ const publicationEntries = deposition.deposition_publications
+ ?.split(',')
+ .map((e) => e.trim())
+ .filter((e) => DOI_ID.exec(e)) // only show DOI links
+
+ const relatedDatabaseEntries = deposition.related_database_entries
+ ?.split(',')
+ .map((e) => e.trim())
+
+ return (
+
+
+
+ {t('depositionOverview')}
+
+
+ {deposition.description}
+
+
+
+
+
+
{t('depositionData')}
+
+
+ {t('annotations')}:
+
+ {annotationsCount.toLocaleString()}
+
+
+
+
+
{t('publications')}
+
+
+
+
+
{t('relatedDatabases')}
+
+
+
+
+
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/Deposition/index.ts b/frontend/packages/data-portal/app/components/Deposition/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/packages/data-portal/app/components/I18n.tsx b/frontend/packages/data-portal/app/components/I18n.tsx
index aa056e32f..b10fbf3eb 100644
--- a/frontend/packages/data-portal/app/components/I18n.tsx
+++ b/frontend/packages/data-portal/app/components/I18n.tsx
@@ -1,14 +1,13 @@
-import { LinkProps } from '@remix-run/react'
import { Trans, TransProps } from 'react-i18next'
import { Required } from 'utility-types'
import type { I18nKeys } from 'app/types/i18n'
-import { Link } from './Link'
+import { Link, VariantLinkProps } from './Link'
interface Props extends Omit, 'ns' | 'i18nKey'> {
i18nKey: I18nKeys
- linkProps?: Partial
+ linkProps?: Partial
}
/**
@@ -28,10 +27,17 @@ export function I18n({ i18nKey, components, linkProps, ...props }: Props) {
semibold: ,
code: ,
+ urlNoColor: (
+ , 'to'>)}>
+ {/* This will get replaced by I18next */}
+ tmp
+
+ ),
+
url: (
, 'to'>)}
+ {...(linkProps as Required, 'to'>)}
>
{/* This will get replaced by I18next */}
tmp
diff --git a/frontend/packages/data-portal/app/components/Link/Link.tsx b/frontend/packages/data-portal/app/components/Link/Link.tsx
index a8776db71..6075fc685 100644
--- a/frontend/packages/data-portal/app/components/Link/Link.tsx
+++ b/frontend/packages/data-portal/app/components/Link/Link.tsx
@@ -10,13 +10,12 @@ export const DASHED_BORDERED_CLASSES =
export const DASHED_UNDERLINED_CLASSES =
'underline underline-offset-[3px] decoration-dashed hover:decoration-solid'
+export type VariantLinkProps = LinkProps & {
+ variant?: 'dashed-bordered' | 'dashed-underlined'
+}
+
function BaseLink(
- {
- to,
- variant,
- className,
- ...props
- }: LinkProps & { variant?: 'dashed-bordered' | 'dashed-underlined' },
+ { to, variant, className, ...props }: VariantLinkProps,
ref: ForwardedRef,
) {
let newTabProps: Partial = {}
diff --git a/frontend/packages/data-portal/app/components/PageHeaderSubtitle.tsx b/frontend/packages/data-portal/app/components/PageHeaderSubtitle.tsx
index c870b9e02..9cf1e1d17 100644
--- a/frontend/packages/data-portal/app/components/PageHeaderSubtitle.tsx
+++ b/frontend/packages/data-portal/app/components/PageHeaderSubtitle.tsx
@@ -1,8 +1,21 @@
import { ReactNode } from 'react'
-export function PageHeaderSubtitle({ children }: { children: ReactNode }) {
+import { cns } from 'app/utils/cns'
+
+export function PageHeaderSubtitle({
+ className,
+ children,
+}: {
+ className: string
+ children: ReactNode
+}) {
return (
-
+
{children}
)
diff --git a/frontend/packages/data-portal/app/components/Run/AnnotationOveriewTable.tsx b/frontend/packages/data-portal/app/components/Run/AnnotationOveriewTable.tsx
index 8fab56c7e..6d23c9af5 100644
--- a/frontend/packages/data-portal/app/components/Run/AnnotationOveriewTable.tsx
+++ b/frontend/packages/data-portal/app/components/Run/AnnotationOveriewTable.tsx
@@ -1,6 +1,6 @@
import { AccordionMetadataTable } from 'app/components/AccordionMetadataTable'
import { AuthorLegend } from 'app/components/AuthorLegend'
-import { DatasetAuthors } from 'app/components/Dataset/DatasetAuthors'
+import { AuthorList } from 'app/components/AuthorList'
import { useI18n } from 'app/hooks/useI18n'
import { useAnnotation } from 'app/state/annotation'
@@ -28,7 +28,7 @@ export function AnnotationOverviewTable() {
: t('annotationAuthors'),
labelExtra: ,
renderValue: () => {
- return
+ return
},
values: [''],
className: 'leading-sds-body-s',
diff --git a/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx b/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx
index 2b59b5c57..40adf0e2a 100644
--- a/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx
+++ b/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx
@@ -5,11 +5,16 @@ import { ColumnDef, createColumnHelper } from '@tanstack/react-table'
import { range } from 'lodash-es'
import { ComponentProps, useCallback, useMemo } from 'react'
-import { DatasetAuthors } from 'app/components/Dataset/DatasetAuthors'
+import { AuthorList } from 'app/components/AuthorList'
import { I18n } from 'app/components/I18n'
import { DASHED_BORDERED_CLASSES } from 'app/components/Link'
import { CellHeader, PageTable, TableCell } from 'app/components/Table'
import { Tooltip } from 'app/components/Tooltip'
+import {
+ methodLabels,
+ methodTooltipLabels,
+ MethodType,
+} from 'app/constants/methodTypes'
import { MAX_PER_PAGE } from 'app/constants/pagination'
import { AnnotationTableWidths } from 'app/constants/table'
import { TestIds } from 'app/constants/testIds'
@@ -58,24 +63,6 @@ function ConfidenceValue({ value }: { value: number }) {
)
}
-type MethodTypeLabels = {
- automated: I18nKeys
- hybrid: I18nKeys
- manual: I18nKeys
-}
-
-const methodLabels: MethodTypeLabels = {
- automated: 'automated',
- hybrid: 'hybrid',
- manual: 'manual',
-}
-
-const methodTooltipLabels: MethodTypeLabels = {
- automated: 'methodTypeAutomated',
- hybrid: 'methodTypeHybrid',
- manual: 'methodTypeManual',
-}
-
export function AnnotationTable() {
const { isLoadingDebounced } = useIsLoading()
const { run } = useRunById()
@@ -184,7 +171,7 @@ export function AnnotationTable() {
),
@@ -257,10 +244,7 @@ export function AnnotationTable() {
)
}
- const methodType = annotation.method_type as
- | 'automated'
- | 'manual'
- | 'hybrid'
+ const methodType = annotation.method_type as MethodType
return (
}
+ breadcrumbs={}
metadata={[{ key: t('runId'), value: `${run.id}` }]}
onMoreInfoClick={() => toggleDrawer(MetadataDrawerId.Run)}
title={run.name}
@@ -123,7 +123,9 @@ export function RunHeader() {
-
{t('runOverview')}
+
+ {t('runOverview')}
+
{multipleTomogramsEnabled ? (
- {children}
+ {children}
+ {subHeader && (
+
+ {subHeader}
+
+ )}
)
}
diff --git a/frontend/packages/data-portal/app/components/icons/SourceCodeIcon.tsx b/frontend/packages/data-portal/app/components/icons/SourceCodeIcon.tsx
new file mode 100644
index 000000000..02ebdce4d
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/icons/SourceCodeIcon.tsx
@@ -0,0 +1,31 @@
+import { IconProps } from './icon.types'
+
+export function SourceCodeIcon(props: IconProps) {
+ return (
+
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/icons/WeightsIcon.tsx b/frontend/packages/data-portal/app/components/icons/WeightsIcon.tsx
new file mode 100644
index 000000000..2f6fc355a
--- /dev/null
+++ b/frontend/packages/data-portal/app/components/icons/WeightsIcon.tsx
@@ -0,0 +1,21 @@
+import { IconProps } from './icon.types'
+
+export function WeightsIcon(props: IconProps) {
+ return (
+
+ )
+}
diff --git a/frontend/packages/data-portal/app/components/icons/index.ts b/frontend/packages/data-portal/app/components/icons/index.ts
index bc95aae2d..3c032aaf7 100644
--- a/frontend/packages/data-portal/app/components/icons/index.ts
+++ b/frontend/packages/data-portal/app/components/icons/index.ts
@@ -7,4 +7,6 @@ export * from './ImageInstituteIcon'
export * from './KeyPhotoFallbackIcon'
export * from './ORCIDIcon'
export * from './SmallChevronRightIcon'
+export * from './SourceCodeIcon'
export * from './SpeechBubbleIcon'
+export * from './WeightsIcon'
diff --git a/frontend/packages/data-portal/app/constants/methodTypes.ts b/frontend/packages/data-portal/app/constants/methodTypes.ts
new file mode 100644
index 000000000..0b3bc12ab
--- /dev/null
+++ b/frontend/packages/data-portal/app/constants/methodTypes.ts
@@ -0,0 +1,21 @@
+import { I18nKeys } from 'app/types/i18n'
+
+type MethodTypeLabels = {
+ automated: I18nKeys
+ hybrid: I18nKeys
+ manual: I18nKeys
+}
+
+export type MethodType = keyof MethodTypeLabels
+
+export const methodLabels: MethodTypeLabels = {
+ automated: 'automated',
+ hybrid: 'hybrid',
+ manual: 'manual',
+}
+
+export const methodTooltipLabels: MethodTypeLabels = {
+ automated: 'methodTypeAutomated',
+ hybrid: 'methodTypeHybrid',
+ manual: 'methodTypeManual',
+}
diff --git a/frontend/packages/data-portal/app/constants/table.ts b/frontend/packages/data-portal/app/constants/table.ts
index 67ced8b4d..c63b7289c 100644
--- a/frontend/packages/data-portal/app/constants/table.ts
+++ b/frontend/packages/data-portal/app/constants/table.ts
@@ -47,3 +47,12 @@ export const DepositionTableWidths = {
annotatedObjects: { min: 120, max: 400 },
objectShapeTypes: { min: 120, max: 200 },
}
+
+export const DepositionPageDatasetTableWidths = {
+ photo: PHOTO_COLUMN_WIDTH,
+ id: { min: 300, max: 800 },
+ organism: { min: 100, max: 400 },
+ runs: { min: 120, max: 200 },
+ annotations: { min: 120, max: 200 },
+ annotatedObjects: { min: 120, max: 400 },
+}
diff --git a/frontend/packages/data-portal/app/graphql/getDepositionById.server.ts b/frontend/packages/data-portal/app/graphql/getDepositionById.server.ts
new file mode 100644
index 000000000..044002f98
--- /dev/null
+++ b/frontend/packages/data-portal/app/graphql/getDepositionById.server.ts
@@ -0,0 +1,167 @@
+import type { ApolloClient, NormalizedCacheObject } from '@apollo/client'
+
+import { gql } from 'app/__generated__'
+import {
+ Datasets_Bool_Exp as Depositions_Bool_Exp,
+ Order_By,
+} from 'app/__generated__/graphql'
+import { MAX_PER_PAGE } from 'app/constants/pagination'
+import { FilterState, getFilterState } from 'app/hooks/useFilter'
+
+const GET_DEPOSITION_BY_ID = gql(`
+ query GetDepositionById(
+ $id: Int!,
+ $dataset_limit: Int,
+ $dataset_offset: Int,
+ $dataset_order_by: order_by,
+ $filter: datasets_bool_exp,
+ ) {
+ deposition: datasets_by_pk(id: $id) {
+ s3_prefix
+
+ # key photo
+ key_photo_url
+
+ # dates
+ last_modified_date
+ release_date
+ deposition_date
+
+ # metadata
+ id
+ title
+ description
+
+ funding_sources {
+ funding_agency_name
+ grant_id
+ }
+
+ related_database_entries
+ deposition_citations: dataset_citations
+
+ authors(
+ order_by: {
+ author_list_order: asc,
+ },
+ ) {
+ corresponding_author_status
+ email
+ name
+ orcid
+ primary_author_status
+ }
+
+ # publication info
+ related_database_entries
+ deposition_publications: dataset_publications
+
+ # runs
+ run_stats: runs {
+ tomogram_voxel_spacings {
+ annotations {
+ object_name
+
+ files(distinct_on: shape_type) {
+ shape_type
+ }
+ }
+
+ annotations_aggregate {
+ aggregate {
+ count
+ }
+ }
+ }
+ }
+ }
+
+ datasets(
+ limit: $dataset_limit,
+ offset: $dataset_offset,
+ order_by: { title: $dataset_order_by },
+ where: $filter
+ ) {
+ id
+ title
+ organism_name
+ key_photo_thumbnail_url
+
+ authors(
+ order_by: {
+ author_list_order: asc,
+ },
+ ) {
+ name
+ primary_author_status
+ corresponding_author_status
+ }
+
+ runs_aggregate {
+ aggregate {
+ count
+ }
+ }
+
+ runs {
+ tomogram_voxel_spacings {
+ annotations(distinct_on: object_name) {
+ object_name
+ }
+
+ annotations_aggregate {
+ aggregate {
+ count
+ }
+ }
+ }
+ }
+ }
+
+ datasets_aggregate {
+ aggregate {
+ count
+ }
+ }
+
+ filtered_datasets_aggregate: datasets_aggregate(where: $filter) {
+ aggregate {
+ count
+ }
+ }
+ }
+`)
+
+function getFilter(filterState: FilterState) {
+ const filters: Depositions_Bool_Exp[] = []
+ // TODO: implement filters
+ // eslint-disable-next-line no-console
+ console.log(filterState)
+
+ return { _and: filters } as Depositions_Bool_Exp
+}
+
+export async function getDepositionById({
+ client,
+ id,
+ orderBy,
+ page = 1,
+ params = new URLSearchParams(),
+}: {
+ client: ApolloClient
+ orderBy?: Order_By | null
+ id: number
+ page?: number
+ params?: URLSearchParams
+}) {
+ return client.query({
+ query: GET_DEPOSITION_BY_ID,
+ variables: {
+ id,
+ dataset_limit: MAX_PER_PAGE,
+ dataset_offset: (page - 1) * MAX_PER_PAGE,
+ dataset_order_by: orderBy,
+ filter: getFilter(getFilterState(params)),
+ },
+ })
+}
diff --git a/frontend/packages/data-portal/app/hooks/useDepositionById.ts b/frontend/packages/data-portal/app/hooks/useDepositionById.ts
new file mode 100644
index 000000000..5ac30a603
--- /dev/null
+++ b/frontend/packages/data-portal/app/hooks/useDepositionById.ts
@@ -0,0 +1,60 @@
+import { useMemo } from 'react'
+import { useTypedLoaderData } from 'remix-typedjson'
+
+import { GetDepositionByIdQuery } from 'app/__generated__/graphql'
+import { NonUndefined } from 'app/types/utils'
+
+export type Dataset = GetDepositionByIdQuery['datasets'][number]
+
+export type Deposition = NonUndefined & {
+ datasets: GetDepositionByIdQuery['datasets']
+ datasets_aggregate: GetDepositionByIdQuery['datasets_aggregate']
+ filtered_datasets_aggregate: GetDepositionByIdQuery['filtered_datasets_aggregate']
+}
+
+export function useDepositionById() {
+ const data = useTypedLoaderData()
+
+ const objectNames = useMemo(
+ () =>
+ Array.from(
+ new Set(
+ data.deposition?.run_stats.flatMap((run) =>
+ run.tomogram_voxel_spacings.flatMap((voxelSpacing) =>
+ voxelSpacing.annotations.flatMap(
+ (annotation) => annotation.object_name,
+ ),
+ ),
+ ),
+ ),
+ ),
+ [data.deposition?.run_stats],
+ )
+
+ const objectShapeTypes = useMemo(
+ () =>
+ Array.from(
+ new Set(
+ data.deposition?.run_stats.flatMap((run) =>
+ run.tomogram_voxel_spacings.flatMap((voxelSpacing) =>
+ voxelSpacing.annotations.flatMap((annotation) =>
+ annotation.files.flatMap((file) => file.shape_type),
+ ),
+ ),
+ ),
+ ),
+ ),
+ [data.deposition?.run_stats],
+ )
+
+ return {
+ deposition: {
+ ...data.deposition,
+ datasets: data.datasets,
+ datasets_aggregate: data.datasets_aggregate,
+ filtered_datasets_aggregate: data.filtered_datasets_aggregate,
+ } as Deposition,
+ objectNames,
+ objectShapeTypes,
+ }
+}
diff --git a/frontend/packages/data-portal/app/hooks/useMetadataDrawer.ts b/frontend/packages/data-portal/app/hooks/useMetadataDrawer.ts
index 31656475c..2cc34bca5 100644
--- a/frontend/packages/data-portal/app/hooks/useMetadataDrawer.ts
+++ b/frontend/packages/data-portal/app/hooks/useMetadataDrawer.ts
@@ -7,6 +7,7 @@ import { stringParam, useQueryParams } from './useQueryParam'
export enum MetadataDrawerId {
Annotation = 'annotation',
Dataset = 'dataset',
+ Deposition = 'deposition',
Run = 'run',
}
diff --git a/frontend/packages/data-portal/app/routes/depositions.$id.tsx b/frontend/packages/data-portal/app/routes/depositions.$id.tsx
new file mode 100644
index 000000000..54ffd0b96
--- /dev/null
+++ b/frontend/packages/data-portal/app/routes/depositions.$id.tsx
@@ -0,0 +1,83 @@
+/* eslint-disable @typescript-eslint/no-throw-literal */
+
+import { CellHeaderDirection } from '@czi-sds/components'
+import { json, LoaderFunctionArgs, redirect } from '@remix-run/server-runtime'
+
+import { Order_By } from 'app/__generated__/graphql'
+import { apolloClient } from 'app/apollo.server'
+import { DatasetsTable } from 'app/components/Deposition/DatasetsTable'
+import { DepositionHeader } from 'app/components/Deposition/DepositionHeader'
+import { TablePageLayout } from 'app/components/TablePageLayout'
+import { QueryParams } from 'app/constants/query'
+import { getDepositionById } from 'app/graphql/getDepositionById.server'
+import { useDepositionById } from 'app/hooks/useDepositionById'
+import { useI18n } from 'app/hooks/useI18n'
+import { getFeatureFlag } from 'app/utils/featureFlags'
+
+export async function loader({ params, request }: LoaderFunctionArgs) {
+ const url = new URL(request.url)
+
+ const showDepositions = getFeatureFlag({
+ env: process.env.ENV,
+ key: 'depositions',
+ params: url.searchParams,
+ })
+
+ if (!showDepositions) {
+ return redirect('/404')
+ }
+
+ const id = params.id ? +params.id : NaN
+ const page = +(url.searchParams.get(QueryParams.Page) ?? '1')
+ const sort = (url.searchParams.get('sort') ?? undefined) as
+ | CellHeaderDirection
+ | undefined
+
+ if (Number.isNaN(+id)) {
+ throw new Response(null, {
+ status: 400,
+ statusText: 'ID is not defined',
+ })
+ }
+
+ let orderBy: Order_By | null = null
+
+ if (sort) {
+ orderBy = sort === 'asc' ? Order_By.Asc : Order_By.Desc
+ }
+
+ const { data } = await getDepositionById({
+ id,
+ orderBy,
+ page,
+ client: apolloClient,
+ params: url.searchParams,
+ })
+
+ if (data.deposition === null) {
+ throw new Response(null, {
+ status: 404,
+ statusText: `Deposition with ID ${id} not found`,
+ })
+ }
+
+ return json(data)
+}
+
+export default function DatasetByIdPage() {
+ const { deposition } = useDepositionById()
+ const { t } = useI18n()
+
+ return (
+ }
+ table={}
+ totalCount={deposition.datasets_aggregate.aggregate?.count ?? 0}
+ />
+ )
+}
diff --git a/frontend/packages/data-portal/app/types/utils.ts b/frontend/packages/data-portal/app/types/utils.ts
new file mode 100644
index 000000000..aabab7d8c
--- /dev/null
+++ b/frontend/packages/data-portal/app/types/utils.ts
@@ -0,0 +1 @@
+export type NonUndefined = T extends undefined ? never : T
diff --git a/frontend/packages/data-portal/public/locales/en/translation.json b/frontend/packages/data-portal/public/locales/en/translation.json
index efe2ce607..6cb85582e 100644
--- a/frontend/packages/data-portal/public/locales/en/translation.json
+++ b/frontend/packages/data-portal/public/locales/en/translation.json
@@ -18,6 +18,7 @@
"alignmentFile": "Alignment File",
"all": "All",
"allDatasets": "All Datasets",
+ "allDepositions": "All Depositions",
"annotatedObjects": "Annotated Objects",
"annotationAuthor": "Annotation Author",
"annotationAuthors": "Annotation Authors",
@@ -26,6 +27,7 @@
"annotationId": "Annotation ID",
"annotationMetadata": "Annotation Metadata",
"annotationMethod": "Annotation Method",
+ "annotationMethodsSummary": "Annotation Methods Summary",
"annotationObject": "Annotation Object",
"annotationOverview": "Annotation Overview",
"annotationSoftware": "Annotation Software",
@@ -98,9 +100,13 @@
"datasets": "Datasets",
"datasetsTab": "Datasets {{count}}",
"dataSummary": "Data Summary",
+ "deposition": "Deposition",
+ "depositionData": "Deposition Data",
"depositionDate": "Deposition Date",
"depositionId": "Deposition ID",
"depositionName": "Deposition Name",
+ "depositionOnly": "Deposition only",
+ "depositionOverview": "Deposition Overview",
"depositions": "Depositions",
"depositionsTab": "Depositions {{count}}",
"description": "Description",
@@ -187,6 +193,7 @@
"manual": "Manual",
"meetsAll": "Meets all",
"metadata": "Metadata",
+ "methodLinks": "Method Links",
"methodType": "Method Type",
"methodTypeAutomated": "Automated: Annotations were generated using automated tools or algorithms without supervision.",
"methodTypeHybrid": "Hybrid: Annotations were generated using a combination of automated and manual methods.",
@@ -194,6 +201,7 @@
"methodTypeManual": "Manual: Annotations were created by hand.",
"microscopeManufacturer": "Microscope Manufacturer",
"microscopeModel": "Microscope model",
+ "modelWeights": "Model Weights",
"moderate": "Moderate",
"moreInfo": "More Info",
"moreInfoComingFall2024": "More info coming in Fall 2024",
@@ -225,6 +233,7 @@
"organismName": "Organism Name",
"organizers": "Organizers",
"orientedPoint": "Oriented Point",
+ "other": "Other",
"otherSetup": "Other Setup",
"participateInOurCompetition": "Participate in our ML competition",
"participateInOurCompetitionCTA": "Develop a ML model for annotating subcellular structures and proteins in CryoET data.",
@@ -265,7 +274,8 @@
"runOverview": "Run Overview",
"runs": "Runs",
"runsTab": "Runs {{count}}",
- "runsTooltip": "Run: A tomography run is a collection of all data and annotations related to one physical location in a biological specimen. Learn More",
+ "runsTooltip": "Run: A tomography run is a collection of all data and annotations related to one physical location in a biological specimen. Learn More",
+ "runsTooltipDepositionSubtext": "Count only shows the number of runs containing deposition data. Learn more about depositions in the portal.",
"sampleAndExperimentConditions": "Sample and Experiment Conditions",
"samplePreparation": "Sample Preparation",
"sampleType": "Sample Type",
@@ -276,10 +286,12 @@
"selectDownloadMethod": "Select Download Method",
"selectSaveDestination": "Select Save Destination",
"seriesIsAligned": "Series is Aligned",
- "showLess": "Show Less",
+ "showLess": "Show less",
+ "showMore": "Show {{count}} more",
"size": "Size",
"smallestAvailableVoxelSpacing": "Smallest Available Voxel Spacing",
"somethingWentWrong": "Something went wrong",
+ "sourceCode": "Source Code",
"species": "Species",
"sphericalAberrationConstant": "Spherical Aberration Constant",
"sponsoredBy": "Sponsored By",
@@ -288,6 +300,7 @@
"submitFeedback": "Submit Feedback",
"surveyBanner": "Share first impressions, or sign up for invites to future feedback activities in this short form.",
"surveyLink": "https://airtable.com/apppmytRJXoXYTO9w/shrjmV9knAC7E7VVM?prefill_Event=Banner&hide_Event=true",
+ "symbolPeriod": ".",
"tellUsMore": "Tell us More",
"terms": "Terms",
"termsOfUse": "Terms of Use",
@@ -303,7 +316,7 @@
"tiltSeriesAlignment": "Tilt Series Alignment",
"tiltSeriesMetadata": "Tilt Series Metadata",
"tiltSeriesQualityScore": "Tilt Series Quality Score",
- "tiltSeriesTooltip": "Tilt Series Quality: Dataset author’s assessment of tilt series quality. Score ranges 1-5, with 5 being best. Learn More",
+ "tiltSeriesTooltip": "Tilt Series Quality: Dataset author's assessment of tilt series quality. Score ranges 1-5, with 5 being best. Learn More",
"tiltStep": "Tilt Step",
"tiltingScheme": "Tilting Scheme",
"tissueName": "Tissue Name",
@@ -338,6 +351,8 @@
"viewDatasetsCta": "Find and visualize cryoET datasets in the portal and download to use for your work.",
"viewTomogram": "View Tomogram",
"voxelSpacingId": "Voxel Spacing ID",
+ "website": "Website",
+ "withDepositionData": "With deposition data",
"yes": "Yes",
"youMustHaveCliInstalled": "You must have AWS CLI installed. How to Install AWS CLI.",
"youMustHaveCurlInstalled": "You must have cURL installed. How to Install cURL",