diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index 5cc95cc6b..5ec9bf40d 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -33,7 +33,7 @@ jobs: const { owner, repo } = context.repo const { data: searchResult } = await github.rest.search.issuesAndPullRequests( { - q: `repo:${owner}/${repo} is:pr label:"autorelease: pending"`, + q: `repo:${owner}/${repo} is:pr is:open label:"autorelease: pending" "chore(main): release web"`, per_page: 1, } ) @@ -63,9 +63,6 @@ jobs: environment: staging tag-frontend-release: - concurrency: - group: tag-frontend-release-${{ github.ref }} - cancel-in-progress: true outputs: tag: ${{ fromJson(steps.get-tag-name.outputs.result).tag }} diff --git a/client/python/cryoet_data_portal/src/cryoet_data_portal/__init__.py b/client/python/cryoet_data_portal/src/cryoet_data_portal/__init__.py index e2ec4e9d9..67da96f9d 100644 --- a/client/python/cryoet_data_portal/src/cryoet_data_portal/__init__.py +++ b/client/python/cryoet_data_portal/src/cryoet_data_portal/__init__.py @@ -4,12 +4,6 @@ For more information on the API, visit the [cryoet-data-portal repo](https://github.com/chanzuckerberg/cryoet-data-portal/) """ -try: - from importlib import metadata -except ImportError: - # for python <=3.7 - import importlib_metadata as metadata # type: ignore[no-redef] - from ._client import Client from ._models import ( Annotation, @@ -24,12 +18,9 @@ TomogramAuthor, TomogramVoxelSpacing, ) +from ._version import version -try: - __version__ = metadata.version("cryoet_data_portal") -except metadata.PackageNotFoundError: - # package is not installed - __version__ = "0.0.0-unknown" +__version__ = version __all__ = [ "Client", diff --git a/client/python/cryoet_data_portal/src/cryoet_data_portal/_client.py b/client/python/cryoet_data_portal/src/cryoet_data_portal/_client.py index 049e3b9a8..2e62507fe 100644 --- a/client/python/cryoet_data_portal/src/cryoet_data_portal/_client.py +++ b/client/python/cryoet_data_portal/src/cryoet_data_portal/_client.py @@ -6,6 +6,8 @@ from gql.dsl import DSLQuery, DSLSchema, dsl_gql from gql.transport.requests import RequestsHTTPTransport +from cryoet_data_portal._constants import USER_AGENT + class Client: """A GraphQL Client library that can traverse all the metadata in the CryoET Data Portal @@ -29,6 +31,7 @@ def __init__(self, url: Optional[str] = None): transport = RequestsHTTPTransport( url=url, retries=3, + headers={"User-agent": USER_AGENT}, ) with open( diff --git a/client/python/cryoet_data_portal/src/cryoet_data_portal/_constants.py b/client/python/cryoet_data_portal/src/cryoet_data_portal/_constants.py new file mode 100644 index 000000000..a149afd0f --- /dev/null +++ b/client/python/cryoet_data_portal/src/cryoet_data_portal/_constants.py @@ -0,0 +1,11 @@ +import sys + +from ._version import version + +# added to all http requests to identify the client for analytics +USER_AGENT = ";".join( + [ + f"python={sys.version_info.major}.{sys.version_info.minor}", + f"cryoet_data_portal_client={version}", + ], +) diff --git a/client/python/cryoet_data_portal/src/cryoet_data_portal/_file_tools.py b/client/python/cryoet_data_portal/src/cryoet_data_portal/_file_tools.py index de9fcf66d..f93f227e7 100644 --- a/client/python/cryoet_data_portal/src/cryoet_data_portal/_file_tools.py +++ b/client/python/cryoet_data_portal/src/cryoet_data_portal/_file_tools.py @@ -9,6 +9,8 @@ from botocore.client import Config from tqdm import tqdm +from cryoet_data_portal._constants import USER_AGENT + logger = logging.getLogger("cryoet-data-portal") @@ -20,10 +22,13 @@ def get_anon_s3_client(): return boto3.client( "s3", endpoint_url=boto_url, - config=Config(signature_version=signature_version), + config=Config(signature_version=signature_version, user_agent=USER_AGENT), ) - return boto3.client("s3", config=Config(signature_version=UNSIGNED)) + return boto3.client( + "s3", + config=Config(signature_version=UNSIGNED, user_agent=USER_AGENT), + ) def parse_s3_url(url: str) -> (str, str): @@ -37,7 +42,7 @@ def download_https( with_progress: bool = True, ): dest_path = get_destination_path(url, dest_path) - fetch_request = requests.get(url, stream=True) + fetch_request = requests.get(url, stream=True, headers={"User-agent": USER_AGENT}) total_size = int(fetch_request.headers["content-length"]) block_size = 1024 * 512 logger.info("Downloading %s to %s", url, dest_path) diff --git a/client/python/cryoet_data_portal/src/cryoet_data_portal/_version.py b/client/python/cryoet_data_portal/src/cryoet_data_portal/_version.py new file mode 100644 index 000000000..3a24e1a75 --- /dev/null +++ b/client/python/cryoet_data_portal/src/cryoet_data_portal/_version.py @@ -0,0 +1,11 @@ +try: + from importlib import metadata +except ImportError: + # for python <=3.7 + import importlib_metadata as metadata # type: ignore[no-redef] + +try: + version = metadata.version("cryoet_data_portal") +except metadata.PackageNotFoundError: + # package is not installed + version = "0.0.0-unknown" diff --git a/client/python/cryoet_data_portal/tests/test_file_tools.py b/client/python/cryoet_data_portal/tests/test_file_tools.py index ceb27db29..96345bfce 100644 --- a/client/python/cryoet_data_portal/tests/test_file_tools.py +++ b/client/python/cryoet_data_portal/tests/test_file_tools.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from unittest.mock import patch import pytest @@ -57,8 +58,13 @@ def test_invalid_dest_path( url = "https://example.com/file.txt" dest_path = os.path.join(tmp_path, "\000") recursive_from_prefix = "https://example.com/" - expected = os.path.join(dest_path, "file.txt") with pytest.raises(ValueError): - assert ( - get_destination_path(url, dest_path, recursive_from_prefix) == expected - ) + get_destination_path(url, dest_path, recursive_from_prefix) + + def test_dest_path_is_existing_file(self, tmp_path) -> None: + """Test that an error is raised if the dest_path is an existing file""" + url = "https://example.com/file.txt" + dest_path = os.path.join(tmp_path, "test.txt") + Path(dest_path).touch() + with pytest.raises(ValueError): + get_destination_path(url, dest_path) diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index ca0e6f08a..d8fa1014e 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -1,5 +1,64 @@ # Changelog +## [1.21.0](https://github.com/chanzuckerberg/cryoet-data-portal/compare/web-v1.20.0...web-v1.21.0) (2024-08-15) + + +### ✨ Features + +* Update Tomogram Processing field format and query ([#1031](https://github.com/chanzuckerberg/cryoet-data-portal/issues/1031)) ([80e1b51](https://github.com/chanzuckerberg/cryoet-data-portal/commit/80e1b51995094f2ee42768c36e6499d11be5b936)) + +## [1.20.0](https://github.com/chanzuckerberg/cryoet-data-portal/compare/web-v1.19.1...web-v1.20.0) (2024-08-15) + + +### ✨ Features + +* Implement collapsing Annotated Objects list ([#1024](https://github.com/chanzuckerberg/cryoet-data-portal/issues/1024)) ([9343d12](https://github.com/chanzuckerberg/cryoet-data-portal/commit/9343d1215a60632696efba551c9272aa7ce98b85)) + + +### 🧹 Miscellaneous Chores + +* Add e2e test for errors on Neuroglancer site ([#1027](https://github.com/chanzuckerberg/cryoet-data-portal/issues/1027)) ([572972d](https://github.com/chanzuckerberg/cryoet-data-portal/commit/572972d915a20338d9a4a59742233f855e3720c8)) + +## [1.19.1](https://github.com/chanzuckerberg/cryoet-data-portal/compare/web-v1.19.0...web-v1.19.1) (2024-08-13) + + +### 🐞 Bug Fixes + +* Dedupe authors ([#1018](https://github.com/chanzuckerberg/cryoet-data-portal/issues/1018)) ([bdbc034](https://github.com/chanzuckerberg/cryoet-data-portal/commit/bdbc034dd20d9db42536e8a84e312545148e6a55)), closes [#752](https://github.com/chanzuckerberg/cryoet-data-portal/issues/752) +* Fix Neuroglancer URL bug ([#1026](https://github.com/chanzuckerberg/cryoet-data-portal/issues/1026)) ([377d6dc](https://github.com/chanzuckerberg/cryoet-data-portal/commit/377d6dcd38870190601ac28d8ddd695f2e8adfb5)), closes [#1025](https://github.com/chanzuckerberg/cryoet-data-portal/issues/1025) + +## [1.19.0](https://github.com/chanzuckerberg/cryoet-data-portal/compare/web-v1.18.0...web-v1.19.0) (2024-08-12) + + +### ✨ Features + +* Add Tomograms table ([#988](https://github.com/chanzuckerberg/cryoet-data-portal/issues/988)) ([420acdd](https://github.com/chanzuckerberg/cryoet-data-portal/commit/420acdd2999e4261214a9475c9a6d37b1c85ef28)) +* Enable pagination of Annotations table with temporary hacky query ([#992](https://github.com/chanzuckerberg/cryoet-data-portal/issues/992)) ([79f7247](https://github.com/chanzuckerberg/cryoet-data-portal/commit/79f724798caacab162eb89a0a4ce57a7b19f5e2d)) + + +### ♻️ Code Refactoring + +* download dialog tests ([#1011](https://github.com/chanzuckerberg/cryoet-data-portal/issues/1011)) ([f0ece55](https://github.com/chanzuckerberg/cryoet-data-portal/commit/f0ece552004cad1d3847691aab0bfd8b0ab6b8e3)), closes [#962](https://github.com/chanzuckerberg/cryoet-data-portal/issues/962) + +## [1.18.0](https://github.com/chanzuckerberg/cryoet-data-portal/compare/web-v1.17.0...web-v1.18.0) (2024-08-02) + + +### ✨ Features + +* Add Tomograms tab to Run page ([#983](https://github.com/chanzuckerberg/cryoet-data-portal/issues/983)) ([c357e6e](https://github.com/chanzuckerberg/cryoet-data-portal/commit/c357e6e804ac31a854a5012295a459c3be19e6e6)) + +## [1.17.0](https://github.com/chanzuckerberg/cryoet-data-portal/compare/web-v1.16.0...web-v1.17.0) (2024-07-31) + + +### ✨ Features + +* Update cell strain links. ([#968](https://github.com/chanzuckerberg/cryoet-data-portal/issues/968)) ([df43bd6](https://github.com/chanzuckerberg/cryoet-data-portal/commit/df43bd68953557c64fa311c9ce417c027acda486)) + + +### 🐞 Bug Fixes + +* avoid double-encoding spaces in breadcrumb url ([#970](https://github.com/chanzuckerberg/cryoet-data-portal/issues/970)) ([8b8a4b4](https://github.com/chanzuckerberg/cryoet-data-portal/commit/8b8a4b4fdedc46b65f86ab2353885151cd53b851)) + ## [1.16.0](https://github.com/chanzuckerberg/cryoet-data-portal/compare/web-v1.15.0...web-v1.16.0) (2024-07-29) diff --git a/frontend/package.json b/frontend/package.json index d88e3ee26..450f30b0d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "1.16.0", + "version": "1.21.0", "scripts": { "preinstall": "npx only-allow pnpm", "build": "pnpm -r build", diff --git a/frontend/packages/data-portal/_templates/e2e/new/pageActor.cjs.t b/frontend/packages/data-portal/_templates/e2e/new/pageActor.cjs.t new file mode 100644 index 000000000..e554ce54f --- /dev/null +++ b/frontend/packages/data-portal/_templates/e2e/new/pageActor.cjs.t @@ -0,0 +1,35 @@ +--- +to: e2e/pageObjects/<%= name %>/<%= name %>Actor.ts +--- +/** + * This file contains combinations of page interactions or data fetching. Remove if not needed. + */ +import { <%= h.changeCase.pascal(name) %>Page } from 'e2e/pageObjects/<%= name %>/<%= name %>Page' + +export class <%= h.changeCase.pascal(name) %>Actor { + private <%= name %>Page: <%= h.changeCase.pascal(name) %>Page + + constructor(<%= name %>Page: <%= h.changeCase.pascal(name) %>Page) { + this.<%= name %>Page = <%= name %>Page + } + // #region Navigate + // #endregion Navigate + + // #region Click + // #endregion Click + + // #region Hover + // #endregion Hover + + // #region Get + // #endregion Get + + // #region Macro + // #endregion Macro + + // #region Validation + // #endregion Validation + + // #region Bool + // #endregion Bool +} diff --git a/frontend/packages/data-portal/_templates/e2e/new/pageObject.cjs.t b/frontend/packages/data-portal/_templates/e2e/new/pageObject.cjs.t index ac716ebef..2f234977c 100644 --- a/frontend/packages/data-portal/_templates/e2e/new/pageObject.cjs.t +++ b/frontend/packages/data-portal/_templates/e2e/new/pageObject.cjs.t @@ -1,6 +1,7 @@ --- to: e2e/pageObjects/<%= name %>/<%= name %>Page.ts --- +import { expect } from '@playwright/test' import { BasePage } from 'e2e/pageObjects/basePage' export class <%= h.changeCase.pascal(name) %>Page extends BasePage { diff --git a/frontend/packages/data-portal/_templates/e2e/new/testFile.cjs.t b/frontend/packages/data-portal/_templates/e2e/new/testFile.cjs.t index a8d7399a0..02be478e3 100644 --- a/frontend/packages/data-portal/_templates/e2e/new/testFile.cjs.t +++ b/frontend/packages/data-portal/_templates/e2e/new/testFile.cjs.t @@ -1,12 +1,19 @@ --- to: e2e/<%= name %>.test.ts --- +import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { test } from '@playwright/test' import { <%= h.changeCase.pascal(name) %>Page } from 'e2e/pageObjects/<%= name %>/<%= name %>Page' test.describe('<%= name %>', () => { - test('should work', async ({ page }) => { - const <%= name %>Page = new <%= h.changeCase.pascal(name) %>Page(page) + let client: ApolloClient + let <%= name %>Page: <%= h.changeCase.pascal(name) %>Page + + test.beforeEach(({page}) => { + <%= name %>Page = new <%= h.changeCase.pascal(name) %>Page(page) + }) + + test('should work', async () => { await <%= name %>Page.goTo('https://playwright.dev/') }) }) diff --git a/frontend/packages/data-portal/app/components/AuthorList/AuthorList.test.tsx b/frontend/packages/data-portal/app/components/AuthorList/AuthorList.test.tsx index 56e9ce1b1..5a744a9ef 100644 --- a/frontend/packages/data-portal/app/components/AuthorList/AuthorList.test.tsx +++ b/frontend/packages/data-portal/app/components/AuthorList/AuthorList.test.tsx @@ -18,108 +18,140 @@ const AUTHOR_MAP = Object.fromEntries( DEFAULT_AUTHORS.map((author) => [author.name, author]), ) -it('should render authors', () => { - render() +describe('non-compact', () => { + it('should render authors', () => { + render() - DEFAULT_AUTHORS.forEach((author) => - expect(screen.getByText(author.name)).toBeInTheDocument(), - ) -}) + DEFAULT_AUTHORS.forEach((author) => + expect(screen.getByText(author.name)).toBeInTheDocument(), + ) + }) -it('should sort primary authors', () => { - render() - const authorNode = screen.getByRole('paragraph') - const authors = (authorNode.textContent ?? '').split(', ') + it('should sort primary authors', () => { + render() + const authors = findAuthorStrings() - expect(AUTHOR_MAP[authors[0]].primary_author_status).toBe(true) - expect(AUTHOR_MAP[authors[1]].primary_author_status).toBe(true) -}) + expect(AUTHOR_MAP[authors[0]].primary_author_status).toBe(true) + expect(AUTHOR_MAP[authors[1]].primary_author_status).toBe(true) + }) -it('should sort other authors', () => { - render() - const authorNode = screen.getByRole('paragraph') - const authors = (authorNode.textContent ?? '').split(', ') - const otherAuthors = authors.slice(2, -2) + it('should sort other authors', () => { + render() + const authors = findAuthorStrings() + const otherAuthors = authors.slice(2, -2) - otherAuthors.forEach((author) => { - expect(AUTHOR_MAP[author].primary_author_status).toBeUndefined() - expect(AUTHOR_MAP[author].corresponding_author_status).toBeUndefined() + otherAuthors.forEach((author) => { + expect(AUTHOR_MAP[author].primary_author_status).toBeUndefined() + expect(AUTHOR_MAP[author].corresponding_author_status).toBeUndefined() + }) }) -}) -it('should sort corresponding authors', () => { - render() - const authorNode = screen.getByRole('paragraph') - const authors = (authorNode.textContent ?? '').split(', ') - - expect(AUTHOR_MAP[authors.at(-1) ?? ''].corresponding_author_status).toBe( - true, - ) - expect(AUTHOR_MAP[authors.at(-2) ?? ''].corresponding_author_status).toBe( - true, - ) -}) + it('should sort corresponding authors', () => { + render() + const authors = findAuthorStrings() -it('should render author links', () => { - const authors = DEFAULT_AUTHORS.map((author, idx) => ({ - ...author, - orcid: `0000-0000-0000-000${idx}`, - })) + expect(AUTHOR_MAP[authors.at(-1) ?? ''].corresponding_author_status).toBe( + true, + ) + expect(AUTHOR_MAP[authors.at(-2) ?? ''].corresponding_author_status).toBe( + true, + ) + }) - render() + it('should render author links', () => { + const authors = DEFAULT_AUTHORS.map((author, idx) => ({ + ...author, + orcid: `0000-0000-0000-000${idx}`, + })) + + render( + , + ) + + authors.forEach((author) => + expect( + screen.getByRole('link', { name: `${author.name}` }), + ).toBeInTheDocument(), + ) + }) - authors.forEach((author) => - expect( - screen.getByRole('link', { name: `${author.name}` }), - ).toBeInTheDocument(), - ) + it('should not display any author more than once', () => { + render( + , + ) + + const authors = findAuthorStrings() + + expect(authors.length).toBe(2) + expect(authors[0]).toBe('One') + expect(authors[1]).toBe('Two') + }) }) -it('should not render author links when compact', () => { - const authors = DEFAULT_AUTHORS.map((author, idx) => ({ - ...author, - orcid: `0000-0000-0000-000${idx}`, - })) - - render( - , - ) - - expect(screen.queryByRole('link')).not.toBeInTheDocument() -}) +describe('compact', () => { + it('should not render author links when compact', () => { + const authors = DEFAULT_AUTHORS.map((author, idx) => ({ + ...author, + orcid: `0000-0000-0000-000${idx}`, + })) + + render( + , + ) + + expect(screen.queryByRole('link')).not.toBeInTheDocument() + }) -it('should not render other authors when compact', () => { - render() - const authorNode = screen.getByRole('paragraph') - const authors = (authorNode.textContent ?? '').split(', ') - const otherAuthors = authors.slice(2, -2) + it('should not render other authors when compact', () => { + render() + const authorNode = screen.getByRole('paragraph') + const authors = (authorNode.textContent ?? '').split(', ') + const otherAuthors = authors.slice(2, -2) - otherAuthors.forEach((author) => - expect(screen.queryByText(author)).not.toBeInTheDocument(), - ) -}) + otherAuthors.forEach((author) => + expect(screen.queryByText(author)).not.toBeInTheDocument(), + ) + }) -it('should render comma if compact and has corresponding authors', () => { - render() - expect(screen.getByText((text) => text.includes('... ,'))).toBeInTheDocument() -}) + it('should render comma if compact and has corresponding authors', () => { + 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, - )} - compact - />, - ) - - expect(screen.getByText((text) => text.includes('...'))).toBeInTheDocument() - expect( - screen.queryByText((text) => text.includes('... ,')), - ).not.toBeInTheDocument() + it('should not render comma for others if compact and no corresponding authors', () => { + render( + !author.corresponding_author_status, + )} + compact + />, + ) + + expect(screen.getByText((text) => text.includes('...'))).toBeInTheDocument() + expect( + screen.queryByText((text) => text.includes('... ,')), + ).not.toBeInTheDocument() + }) }) + +function findAuthorStrings(): string[] { + return (screen.getByRole('paragraph').textContent ?? '').split(', ') +} diff --git a/frontend/packages/data-portal/app/components/AuthorList/AuthorList.tsx b/frontend/packages/data-portal/app/components/AuthorList/AuthorList.tsx index 67dc88cbe..431dd401b 100644 --- a/frontend/packages/data-portal/app/components/AuthorList/AuthorList.tsx +++ b/frontend/packages/data-portal/app/components/AuthorList/AuthorList.tsx @@ -28,17 +28,18 @@ export function AuthorList({ large?: boolean subtle?: boolean }) { - // TODO: make the below grouping more efficient and/or use GraphQL ordering - const authorsPrimary = authors.filter( - (author) => author.primary_author_status, - ) - const authorsCorresponding = authors.filter( - (author) => author.corresponding_author_status, - ) - const authorsOther = authors.filter( - (author) => - !(author.primary_author_status || author.corresponding_author_status), - ) + const authorsPrimary = [] + const authorsOther = [] + const authorsCorresponding = [] + for (const author of authors) { + if (author.primary_author_status) { + authorsPrimary.push(author) + } else if (author.corresponding_author_status) { + authorsCorresponding.push(author) + } else { + authorsOther.push(author) + } + } const otherCollapsed = useMemo(() => { const ellipsis = '...' diff --git a/frontend/packages/data-portal/app/components/Breadcrumbs.tsx b/frontend/packages/data-portal/app/components/Breadcrumbs.tsx index 530bef698..88aa61791 100644 --- a/frontend/packages/data-portal/app/components/Breadcrumbs.tsx +++ b/frontend/packages/data-portal/app/components/Breadcrumbs.tsx @@ -12,9 +12,7 @@ import { cns } from 'app/utils/cns' function encodeParams(params: [string, string | null][]): string { const searchParams = new URLSearchParams( - (params.filter((kv) => isString(kv[1])) as string[][]).map((kv) => - kv.map(encodeURIComponent), - ), + params.filter((kv) => isString(kv[1])) as string[][], ) return searchParams.toString() diff --git a/frontend/packages/data-portal/app/components/CollapsibleList.tsx b/frontend/packages/data-portal/app/components/CollapsibleList.tsx index 3b88fbdaa..cf49af2d4 100644 --- a/frontend/packages/data-portal/app/components/CollapsibleList.tsx +++ b/frontend/packages/data-portal/app/components/CollapsibleList.tsx @@ -9,40 +9,60 @@ interface ListEntry { entry: ReactNode } +export interface CollapsibleListProps { + entries?: ListEntry[] + + // Number of items displayed when collapsed. + // Collapse triggers when entries has >= collapseAfter + 2 items, so minimum "Show _ more" value + // is 2. + collapseAfter?: number + + inlineVariant?: boolean + tableVariant?: boolean +} + export function CollapsibleList({ entries, collapseAfter, + inlineVariant = false, tableVariant = false, -}: { - entries?: ListEntry[] - collapseAfter?: number - tableVariant?: boolean -}) { +}: CollapsibleListProps) { const collapsible = collapseAfter !== undefined && collapseAfter >= 0 && entries !== undefined && - entries.length > collapseAfter + 1 + entries.length > collapseAfter + 1 // Prevent "Show 1 more" const { t } = useI18n() const [collapsed, setCollapsed] = useState(true) + const lastIndex = + collapsible && collapsed ? collapseAfter - 1 : (entries ?? []).length - 1 + return entries ? ( -
    - {entries.map( - ({ key, entry }, i) => - !(collapsible && collapsed && i + 1 > collapseAfter) && ( -
  • {entry}
  • - ), - )} + <> +
      + {entries.slice(0, lastIndex + 1).map(({ key, entry }, i) => ( +
    • + {entry} + {inlineVariant && i !== lastIndex && ', '} + {inlineVariant && + collapsible && + collapsed && + i === lastIndex && + '...'} +
    • + ))} +
    {collapsible && (
    )} -
+ ) : (

{t('notSubmitted')} diff --git a/frontend/packages/data-portal/app/components/DatasetFilter/AnnotationMetadataFilterSection.tsx b/frontend/packages/data-portal/app/components/DatasetFilter/AnnotationMetadataFilterSection.tsx index 8f8bc253e..74fd6d4cb 100644 --- a/frontend/packages/data-portal/app/components/DatasetFilter/AnnotationMetadataFilterSection.tsx +++ b/frontend/packages/data-portal/app/components/DatasetFilter/AnnotationMetadataFilterSection.tsx @@ -1,23 +1,36 @@ +import { GeneOntologyFilter } from 'app/components/AnnotationFilter/GeneOntologyFilter' import { AnnotatedObjectNameFilter, AnnotatedObjectShapeTypeFilter, FilterSection, } from 'app/components/Filters' -import { useDatasets } from 'app/hooks/useDatasets' +import { useDatasetsFilterData } from 'app/hooks/useDatasetsFilterData' import { useI18n } from 'app/hooks/useI18n' -export function AnnotationMetadataFilterSection() { - const { objectNames, objectShapeTypes } = useDatasets() +export function AnnotationMetadataFilterSection({ + depositionPageVariant, +}: { + depositionPageVariant?: boolean +}) { + const { objectNames, objectShapeTypes } = useDatasetsFilterData() const { t } = useI18n() return ( + {depositionPageVariant && ( +

+ {t('depositionAnnotationsOnly')} +

+ )} + + {depositionPageVariant && } + ) diff --git a/frontend/packages/data-portal/app/components/DatasetFilter/DatasetFilter.tsx b/frontend/packages/data-portal/app/components/DatasetFilter/DatasetFilter.tsx index 349c0a7f4..21c68fd84 100644 --- a/frontend/packages/data-portal/app/components/DatasetFilter/DatasetFilter.tsx +++ b/frontend/packages/data-portal/app/components/DatasetFilter/DatasetFilter.tsx @@ -1,6 +1,6 @@ +import { ErrorBoundary } from 'app/components/ErrorBoundary' import { FilterPanel } from 'app/components/Filters' -import { ErrorBoundary } from '../ErrorBoundary' import { AnnotationMetadataFilterSection } from './AnnotationMetadataFilterSection' import { HardwareFilterSection } from './HardwareFilterSection' import { IncludedContentsFilterSection } from './IncludedContentsFilterSection' @@ -9,11 +9,19 @@ import { SampleAndExperimentFilterSection } from './SampleAndExperimentFilterSec import { TiltSeriesMetadataFilterSection } from './TiltSeriesMetadataFilterSection' import { TomogramMetadataFilterSection } from './TomogramMetadataFilterSection' -export function DatasetFilter() { +export function DatasetFilter({ + depositionPageVariant, +}: { + depositionPageVariant?: boolean +}) { const filters = [ { logId: 'included-contents-filter', - filter: , + filter: ( + + ), }, { logId: 'name-or-id-filter', @@ -37,7 +45,11 @@ export function DatasetFilter() { }, { logId: 'annotation-metadata-filter', - filter: , + filter: ( + + ), }, ] diff --git a/frontend/packages/data-portal/app/components/DatasetFilter/HardwareFilterSection.tsx b/frontend/packages/data-portal/app/components/DatasetFilter/HardwareFilterSection.tsx index b7c1e681b..62c427ec9 100644 --- a/frontend/packages/data-portal/app/components/DatasetFilter/HardwareFilterSection.tsx +++ b/frontend/packages/data-portal/app/components/DatasetFilter/HardwareFilterSection.tsx @@ -2,13 +2,13 @@ import { useMemo } from 'react' import { FilterSection, SelectFilter } from 'app/components/Filters' import { QueryParams } from 'app/constants/query' -import { useDatasets } from 'app/hooks/useDatasets' +import { useDatasetsFilterData } from 'app/hooks/useDatasetsFilterData' import { useFilter } from 'app/hooks/useFilter' import { i18n } from 'app/i18n' import { BaseFilterOption } from 'app/types/filter' export function HardwareFilterSection() { - const { cameraManufacturers } = useDatasets() + const { cameraManufacturers } = useDatasetsFilterData() const cameraManufacturerOptions = useMemo( () => cameraManufacturers.map((value) => ({ value })), diff --git a/frontend/packages/data-portal/app/components/DatasetFilter/IncludedContentsFilterSection.tsx b/frontend/packages/data-portal/app/components/DatasetFilter/IncludedContentsFilterSection.tsx index 072b497a4..2313b52f1 100644 --- a/frontend/packages/data-portal/app/components/DatasetFilter/IncludedContentsFilterSection.tsx +++ b/frontend/packages/data-portal/app/components/DatasetFilter/IncludedContentsFilterSection.tsx @@ -27,7 +27,11 @@ const NUMBER_OF_RUN_OPTIONS: NumberOfRunsFilterOption[] = [ const AVAILABLE_FILES_CLASS_NAME = 'select-available-files' const MEETS_ALL_LABEL_ID = 'meets-all' -export function IncludedContentsFilterSection() { +export function IncludedContentsFilterSection({ + depositionPageVariant, +}: { + depositionPageVariant?: boolean +}) { const { updateValue, includedContents: { availableFiles, numberOfRuns }, @@ -95,7 +99,9 @@ export function IncludedContentsFilterSection() { return ( - + + {t('withDepositionData')} +

+ ) : undefined + } />
) diff --git a/frontend/packages/data-portal/app/components/DatasetFilter/SampleAndExperimentFilterSection.tsx b/frontend/packages/data-portal/app/components/DatasetFilter/SampleAndExperimentFilterSection.tsx index 5ff31ef53..7481e820e 100644 --- a/frontend/packages/data-portal/app/components/DatasetFilter/SampleAndExperimentFilterSection.tsx +++ b/frontend/packages/data-portal/app/components/DatasetFilter/SampleAndExperimentFilterSection.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { FilterSection, SelectFilter } from 'app/components/Filters' import { QueryParams } from 'app/constants/query' -import { useDatasets } from 'app/hooks/useDatasets' +import { useDatasetsFilterData } from 'app/hooks/useDatasetsFilterData' import { useFilter } from 'app/hooks/useFilter' import { i18n } from 'app/i18n' import { BaseFilterOption } from 'app/types/filter' @@ -12,7 +12,7 @@ export function SampleAndExperimentFilterSection() { updateValue, sampleAndExperimentConditions: { organismNames }, } = useFilter() - const { organismNames: allOrganismNames } = useDatasets() + const { organismNames: allOrganismNames } = useDatasetsFilterData() const organismNameOptions = useMemo( () => allOrganismNames.map((name) => ({ value: name })), diff --git a/frontend/packages/data-portal/app/components/DatasetFilter/TomogramMetadataFilterSection.tsx b/frontend/packages/data-portal/app/components/DatasetFilter/TomogramMetadataFilterSection.tsx index 16b61f75c..ec2dae486 100644 --- a/frontend/packages/data-portal/app/components/DatasetFilter/TomogramMetadataFilterSection.tsx +++ b/frontend/packages/data-portal/app/components/DatasetFilter/TomogramMetadataFilterSection.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { FilterSection, SelectFilter } from 'app/components/Filters' import { QueryParams } from 'app/constants/query' -import { useDatasets } from 'app/hooks/useDatasets' +import { useDatasetsFilterData } from 'app/hooks/useDatasetsFilterData' import { useFilter } from 'app/hooks/useFilter' import { i18n } from 'app/i18n' import { @@ -36,7 +36,8 @@ export function TomogramMetadataFilterSection() { )! }, [fiducialAlignmentStatus]) - const { reconstructionMethods, reconstructionSoftwares } = useDatasets() + const { reconstructionMethods, reconstructionSoftwares } = + useDatasetsFilterData() const reconstructionMethodOptions = useMemo( () => reconstructionMethods.map((value) => ({ value })), diff --git a/frontend/packages/data-portal/app/components/Filters/BooleanFilter.tsx b/frontend/packages/data-portal/app/components/Filters/BooleanFilter.tsx index 661941321..16ae4335a 100644 --- a/frontend/packages/data-portal/app/components/Filters/BooleanFilter.tsx +++ b/frontend/packages/data-portal/app/components/Filters/BooleanFilter.tsx @@ -5,10 +5,12 @@ import { cns } from 'app/utils/cns' import styles from './Filters.module.css' export function BooleanFilter({ + caption, label, onChange, value, }: { + caption?: string label: string onChange(value: boolean): void value: boolean @@ -19,6 +21,7 @@ export function BooleanFilter({ checked={value} onChange={(event) => onChange(event.target.checked)} label={label} + caption={caption} /> ) diff --git a/frontend/packages/data-portal/app/components/Filters/GroundTruthAnnotationFilter.tsx b/frontend/packages/data-portal/app/components/Filters/GroundTruthAnnotationFilter.tsx index 5635689c9..c134bf54f 100644 --- a/frontend/packages/data-portal/app/components/Filters/GroundTruthAnnotationFilter.tsx +++ b/frontend/packages/data-portal/app/components/Filters/GroundTruthAnnotationFilter.tsx @@ -4,7 +4,11 @@ import { useI18n } from 'app/hooks/useI18n' import { BooleanFilter } from './BooleanFilter' -export function GroundTruthAnnotationFilter() { +export function GroundTruthAnnotationFilter({ + depositionPageVariant, +}: { + depositionPageVariant?: boolean +}) { const { t } = useI18n() const { updateValue, @@ -12,12 +16,24 @@ export function GroundTruthAnnotationFilter() { } = useFilter() return ( - - updateValue(QueryParams.GroundTruthAnnotation, value ? 'true' : null) - } - value={isGroundTruthEnabled} - /> + <> + + updateValue(QueryParams.GroundTruthAnnotation, value ? 'true' : null) + } + value={isGroundTruthEnabled} + // FIXME: once sds upgraded to 0.20.x uncomment this + // caption={ + // depositionPageVariant ? t('depositionAnnotationsOnly') : undefined + // } + /> + {/* FIXME: once sds upgraded to 0.20.x delete below line and remove fragment wrapper */} + {depositionPageVariant && ( +

+ {t('depositionAnnotationsOnly')} +

+ )} + ) } diff --git a/frontend/packages/data-portal/app/components/Filters/SelectFilter.tsx b/frontend/packages/data-portal/app/components/Filters/SelectFilter.tsx index 1666e9a97..281cd3333 100644 --- a/frontend/packages/data-portal/app/components/Filters/SelectFilter.tsx +++ b/frontend/packages/data-portal/app/components/Filters/SelectFilter.tsx @@ -4,7 +4,7 @@ import { Value, } from '@czi-sds/components' import { isArray, isEqual } from 'lodash-es' -import { useCallback, useMemo } from 'react' +import { ReactNode, useCallback, useMemo } from 'react' import { BaseFilterOption } from 'app/types/filter' import { cns } from 'app/utils/cns' @@ -20,6 +20,7 @@ export function SelectFilter< Multiple extends boolean = false, >({ className, + details, groupBy: groupByProp, label, multiple, @@ -31,6 +32,7 @@ export function SelectFilter< value, }: { className?: string + details?: ReactNode groupBy?: (option: Value) => string label: string multiple?: Multiple @@ -109,6 +111,15 @@ export function SelectFilter< className: cns(popperClassName, multiple && styles.popper), }, }} + InputDropdownProps={ + details + ? { + value: details, + sdsStyle: 'minimal', + sdsType: 'label', + } + : undefined + } onChange={(nextOptions) => { if (isEqual(nextOptions, sdsValue)) { return diff --git a/frontend/packages/data-portal/app/components/PageHeader.tsx b/frontend/packages/data-portal/app/components/PageHeader.tsx index 286412689..2272f7bcf 100644 --- a/frontend/packages/data-portal/app/components/PageHeader.tsx +++ b/frontend/packages/data-portal/app/components/PageHeader.tsx @@ -27,7 +27,7 @@ export function PageHeader({ const { t } = useI18n() return ( -
+
(() => ({ +const LOADING_ANNOTATIONS = range(0, MAX_PER_PAGE).map(() => ({ annotation_method: '', author_affiliations: [], authors_aggregate: {}, @@ -73,7 +73,7 @@ function ConfidenceValue({ value }: { value: number }) { export function AnnotationTable() { const { isLoadingDebounced } = useIsLoading() const [searchParams] = useSearchParams() - const { run, annotationFilesAggregates } = useRunById() + const { run, annotationFiles, annotationFilesAggregates } = useRunById() const { toggleDrawer } = useMetadataDrawer() const { setActiveAnnotation } = useAnnotation() const { t } = useI18n() @@ -81,7 +81,7 @@ export function AnnotationTable() { const { openAnnotationDownloadModal } = useDownloadModalQueryParamState() const openAnnotationDrawer = useCallback( - (annotation: Annotation) => { + (annotation: AnnotationRow) => { setActiveAnnotation(annotation) toggleDrawer(MetadataDrawerId.Annotation) }, @@ -89,7 +89,7 @@ export function AnnotationTable() { ) const columns = useMemo(() => { - const columnHelper = createColumnHelper() + const columnHelper = createColumnHelper() function getConfidenceCell({ cellHeaderProps, @@ -99,7 +99,7 @@ export function AnnotationTable() { }: { cellHeaderProps?: Partial> header: string - key: keyof Annotation + key: keyof AnnotationRow tooltipI18nKey?: I18nKeys }) { return columnHelper.accessor(key, { @@ -331,11 +331,7 @@ export function AnnotationTable() { runId: run.id, annotationId: annotation.id, objectShapeType: annotation.shape_type, - fileFormat: annotation.files - .filter( - (file) => file.shape_type === annotation.shape_type, - ) - .at(0)?.format, + fileFormat: annotation.format, }) } startIcon={ @@ -353,7 +349,7 @@ export function AnnotationTable() { ), }), - ] as ColumnDef[] + ] as ColumnDef[] }, [ t, openAnnotationDrawer, @@ -364,30 +360,14 @@ export function AnnotationTable() { const annotations = useMemo( () => - run.annotation_table.flatMap((data) => - data.annotations.flatMap((annotation) => { - const shapeTypeSet = new Set() - - // Some annotations have files with different shape types. We display each shape type as a separate row. - // This loops through the files and adds an annotation for each shape type. - // If the shape type is filtered out, the files will not be returned in the 'run' object - const files = annotation.files.filter((file) => { - // If the shape type has already been added, don't add another annotation for it - if (shapeTypeSet.has(file.shape_type)) { - return false - } - - shapeTypeSet.add(file.shape_type) - return true - }) - - return files.flatMap((file) => ({ - ...annotation, - ...file, - })) - }), - ) as Annotation[], - [run.annotation_table], + annotationFiles.map((annotationFile) => { + const { annotation: _, ...restAnnotationFileFields } = annotationFile + return { + ...restAnnotationFileFields, + ...annotationFile.annotation, + } as AnnotationRow + }), + [annotationFiles], ) const currentPage = toNumber( @@ -402,8 +382,8 @@ export function AnnotationTable() { * - The non ground truth divider is attached to the first non ground truth row. */ const getGroundTruthDividersForRow = ( - table: Table, - row: Row, + table: Table, + row: Row, ): ReactNode => { return ( <> diff --git a/frontend/packages/data-portal/app/components/Run/RunHeader.tsx b/frontend/packages/data-portal/app/components/Run/RunHeader.tsx index 46ee2801e..2e490d374 100644 --- a/frontend/packages/data-portal/app/components/Run/RunHeader.tsx +++ b/frontend/packages/data-portal/app/components/Run/RunHeader.tsx @@ -1,5 +1,4 @@ import { Button, Icon } from '@czi-sds/components' -import { sum } from 'lodash-es' import { Breadcrumbs } from 'app/components/Breadcrumbs' import { I18n } from 'app/components/I18n' @@ -19,9 +18,12 @@ import { } from 'app/hooks/useMetadataDrawer' import { useRunById } from 'app/hooks/useRunById' import { i18n } from 'app/i18n' +import { TableDataValue } from 'app/types/table' import { useFeatureFlag } from 'app/utils/featureFlags' import { getTiltRangeLabel } from 'app/utils/tiltSeries' +import { CollapsibleList } from '../CollapsibleList' + interface FileSummaryData { key: string value: number @@ -42,7 +44,8 @@ function FileSummary({ data }: { data: FileSummaryData[] }) { export function RunHeader() { const multipleTomogramsEnabled = useFeatureFlag('multipleTomograms') - const { run, annotationFilesAggregates } = useRunById() + const { run, processingMethods, annotationFilesAggregates, tomogramsCount } = + useRunById() const { toggleDrawer } = useMetadataDrawer() const { t } = useI18n() @@ -56,11 +59,6 @@ export function RunHeader() { const framesCount = run.tiltseries_aggregate.aggregate?.sum?.frames_count ?? 0 const tiltSeriesCount = run.tiltseries_aggregate.aggregate?.count ?? 0 - const tomogramsCount = sum( - run.tomogram_stats.flatMap( - (stats) => stats.tomograms_aggregate.aggregate?.count ?? 0, - ), - ) const annotationsCount = annotationFilesAggregates.totalCount return ( @@ -240,9 +238,8 @@ export function RunHeader() { }, { label: i18n.tomogramProcessing, - values: run.tomogram_stats - .flatMap((stats) => stats.tomogram_processing) - .map((tomo) => tomo.processing), + values: processingMethods, + className: 'capitalize', }, { label: i18n.annotatedObjects, @@ -254,6 +251,16 @@ export function RunHeader() { .map((annotation) => annotation.object_name), ), ), + renderValues: (values: TableDataValue[]) => ( + ({ + key: value.toString(), + entry: value.toString(), + }))} + inlineVariant + collapseAfter={6} + /> + ), }, ]} /> diff --git a/frontend/packages/data-portal/app/components/Run/RunMetadataDrawer.tsx b/frontend/packages/data-portal/app/components/Run/RunMetadataDrawer.tsx index d1e2af785..8a8ed516b 100644 --- a/frontend/packages/data-portal/app/components/Run/RunMetadataDrawer.tsx +++ b/frontend/packages/data-portal/app/components/Run/RunMetadataDrawer.tsx @@ -6,7 +6,7 @@ import { useRunById } from 'app/hooks/useRunById' import { i18n } from 'app/i18n' import { RunTiltSeriesTable } from './RunTiltSeriesTable' -import { TomogramsTable } from './TomogramsTable' +import { TomogramsMetadataSection } from './TomogramsMetadataSection' export function RunMetadataDrawer() { const { run } = useRunById() @@ -27,7 +27,7 @@ export function RunMetadataDrawer() { initialOpen={false} /> - + ) } diff --git a/frontend/packages/data-portal/app/components/Run/TomogramMetadataDrawer.tsx b/frontend/packages/data-portal/app/components/Run/TomogramMetadataDrawer.tsx new file mode 100644 index 000000000..6d5967d72 --- /dev/null +++ b/frontend/packages/data-portal/app/components/Run/TomogramMetadataDrawer.tsx @@ -0,0 +1,23 @@ +import { useAtom } from 'jotai' + +import { useI18n } from 'app/hooks/useI18n' +import { MetadataDrawerId } from 'app/hooks/useMetadataDrawer' +import { metadataDrawerTomogramAtom } from 'app/state/metadataDrawerTomogram' + +import { MetadataDrawer } from '../MetadataDrawer' + +export function TomogramMetadataDrawer() { + const { t } = useI18n() + const [metadataDrawerTomogram] = useAtom(metadataDrawerTomogramAtom) + + return ( + + <> + + ) +} diff --git a/frontend/packages/data-portal/app/components/Run/TomogramTable.tsx b/frontend/packages/data-portal/app/components/Run/TomogramTable.tsx new file mode 100644 index 000000000..9364c79c6 --- /dev/null +++ b/frontend/packages/data-portal/app/components/Run/TomogramTable.tsx @@ -0,0 +1,198 @@ +/* eslint-disable react/no-unstable-nested-components */ + +import { Button, Icon } from '@czi-sds/components' +import { ColumnDef, createColumnHelper } from '@tanstack/react-table' +import { useAtom } from 'jotai' +import { useCallback, useMemo } from 'react' + +import { CellHeader, PageTable, TableCell } from 'app/components/Table' +import { TomogramTableWidths } from 'app/constants/table' +import { useDownloadModalQueryParamState } from 'app/hooks/useDownloadModalQueryParamState' +import { useI18n } from 'app/hooks/useI18n' +import { + MetadataDrawerId, + useMetadataDrawer, +} from 'app/hooks/useMetadataDrawer' +import { useRunById } from 'app/hooks/useRunById' +import { + metadataDrawerTomogramAtom, + Tomogram, +} from 'app/state/metadataDrawerTomogram' +import { getNeuroglancerUrl } from 'app/utils/url' + +import { AuthorList } from '../AuthorList' +import { KeyPhoto } from '../KeyPhoto' + +export function TomogramsTable() { + const { t } = useI18n() + const { tomograms } = useRunById() + + const { toggleDrawer } = useMetadataDrawer() + const [, setMetadataDrawerTomogram] = useAtom(metadataDrawerTomogramAtom) + + const { openTomogramDownloadModal } = useDownloadModalQueryParamState() + + const openMetadataDrawer = useCallback( + (tomogram: Tomogram) => { + setMetadataDrawerTomogram(tomogram) + toggleDrawer(MetadataDrawerId.Tomogram) + }, + [setMetadataDrawerTomogram, toggleDrawer], + ) + + const columns = useMemo(() => { + const columnHelper = createColumnHelper() + return [ + columnHelper.accessor('key_photo_url', { + header: () => , + cell: ({ row: { original } }) => ( + + + + ), + }), + columnHelper.accessor('id', { + header: () => ( + + {t('tomogramId')} + + ), + cell: ({ row: { original } }) => ( + +
+

+ {original.id} +

+
+
+ +
+
+ ), + }), + // TODO(bchu): Switch to deposition_date when available. + columnHelper.accessor('name', { + id: 'deposition_date', + header: () => ( + + {t('depositionDate')} + + ), + cell: ({ getValue }) => ( + +
{getValue()}
+
+ ), + }), + // TODO(bchu): Switch to alignment_id when available. + columnHelper.accessor('name', { + header: () => ( + + {t('alignmentId')} + + ), + cell: ({ getValue }) => ( + +
{getValue()}
+
+ ), + }), + columnHelper.accessor('voxel_spacing', { + header: () => ( + + {t('voxelSpacing')} + + ), + cell: ({ getValue, row: { original } }) => ( + + {t('unitAngstrom', { value: getValue() })} +
+ ({original.size_x}, {original.size_y}, {original.size_z})px +
+
+ ), + }), + columnHelper.accessor('reconstruction_method', { + header: () => ( + + {t('reconstructionMethod')} + + ), + cell: ({ getValue }) => ( + +
{getValue()}
+
+ ), + }), + columnHelper.accessor('processing', { + header: () => ( + + {t('postProcessing')} + + ), + cell: ({ getValue }) => ( + +
{getValue()}
+
+ ), + }), + columnHelper.display({ + id: 'tomogram-actions', + header: () => , + cell: ({ row: { original } }) => ( + +
+ {original.is_canonical && + original.neuroglancer_config != null && ( + + )} + + +
+
+ ), + }), + ] as ColumnDef[] // https://github.com/TanStack/table/issues/4382 + }, [openMetadataDrawer, openTomogramDownloadModal, t]) + + return +} diff --git a/frontend/packages/data-portal/app/components/Run/TomogramsTable.tsx b/frontend/packages/data-portal/app/components/Run/TomogramsMetadataSection.tsx similarity index 97% rename from frontend/packages/data-portal/app/components/Run/TomogramsTable.tsx rename to frontend/packages/data-portal/app/components/Run/TomogramsMetadataSection.tsx index 4ad5b22e2..e6d1ccf26 100644 --- a/frontend/packages/data-portal/app/components/Run/TomogramsTable.tsx +++ b/frontend/packages/data-portal/app/components/Run/TomogramsMetadataSection.tsx @@ -6,7 +6,7 @@ import { isFiducial } from 'app/utils/tomograms' import { Matrix4x4 } from './Matrix4x4' -export function TomogramsTable() { +export function TomogramsMetadataSection() { const { run } = useRunById() const tomo = run.tomogram_voxel_spacings.at(0)?.tomograms.at(0) diff --git a/frontend/packages/data-portal/app/components/Table/MetadataTable.tsx b/frontend/packages/data-portal/app/components/Table/MetadataTable.tsx index 7a654cd44..3a22b4090 100644 --- a/frontend/packages/data-portal/app/components/Table/MetadataTable.tsx +++ b/frontend/packages/data-portal/app/components/Table/MetadataTable.tsx @@ -74,23 +74,26 @@ export function MetadataTable({ {datum.renderValue?.(values[0]) ?? values[0]} )) - .otherwise(() => ( -
    - {values.map((value, valueIdx) => ( -
  • - {datum.renderValue?.(value) ?? value} - {valueIdx < values.length - 1 && ', '} -
  • - ))} -
- ))} + .otherwise( + () => + datum.renderValues?.(values) ?? ( +
    + {values.map((value, valueIdx) => ( +
  • + {datum.renderValue?.(value) ?? value} + {valueIdx < values.length - 1 && ', '} +
  • + ))} +
+ ), + )} ) diff --git a/frontend/packages/data-portal/app/components/TablePageLayout.tsx b/frontend/packages/data-portal/app/components/TablePageLayout.tsx index 24ab8c9c4..5a557e1f9 100644 --- a/frontend/packages/data-portal/app/components/TablePageLayout.tsx +++ b/frontend/packages/data-portal/app/components/TablePageLayout.tsx @@ -58,8 +58,12 @@ export function TablePageLayout({ {header} {tabs.length > 1 && ( - <> - {tabsTitle &&
{tabsTitle}
} +
+ {tabsTitle && ( +
+ {tabsTitle} +
+ )} { @@ -70,18 +74,19 @@ export function TablePageLayout({ }} tabs={tabs.map((tab) => ({ label: ( - <> +
{tab.title} - + {tab.filteredCount} - +
), value: tab.title, }))} /> - +
)} + {drawers} diff --git a/frontend/packages/data-portal/app/components/ViewTomogramButton.tsx b/frontend/packages/data-portal/app/components/ViewTomogramButton.tsx index 46e706c7e..a64f7baf1 100644 --- a/frontend/packages/data-portal/app/components/ViewTomogramButton.tsx +++ b/frontend/packages/data-portal/app/components/ViewTomogramButton.tsx @@ -2,6 +2,7 @@ import { Button, ButtonProps } from '@czi-sds/components' import { useI18n } from 'app/hooks/useI18n' import { EventPayloads, Events, usePlausible } from 'app/hooks/usePlausible' +import { getNeuroglancerUrl } from 'app/utils/url' import { Link } from './Link' @@ -45,9 +46,7 @@ export function ViewTomogramButton({ onMouseLeave={() => setIsHoveringOver?.(false)} >