diff --git a/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx b/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx index 7fa2b70f2..9995e6735 100644 --- a/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx +++ b/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx @@ -33,11 +33,11 @@ import { useMetadataDrawer, } from 'app/hooks/useMetadataDrawer' import { useRunById } from 'app/hooks/useRunById' -import { Annotation, useAnnotation } from 'app/state/annotation' +import { AnnotationRow, useAnnotation } from 'app/state/annotation' import { I18nKeys } from 'app/types/i18n' import { cns, cnsNoMerge } from 'app/utils/cns' -const LOADING_ANNOTATIONS = range(0, MAX_PER_PAGE).map(() => ({ +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/context/DownloadModal.context.ts b/frontend/packages/data-portal/app/context/DownloadModal.context.ts index f355ff2a7..a863aa233 100644 --- a/frontend/packages/data-portal/app/context/DownloadModal.context.ts +++ b/frontend/packages/data-portal/app/context/DownloadModal.context.ts @@ -1,7 +1,7 @@ import { createContext, useContext } from 'react' import { GetRunByIdQuery } from 'app/__generated__/graphql' -import { Annotation } from 'app/state/annotation' +import { BaseAnnotation } from 'app/state/annotation' export type DownloadModalType = 'dataset' | 'runs' | 'annotation' @@ -9,7 +9,7 @@ export type TomogramResolution = GetRunByIdQuery['runs'][number]['tomogram_stats'][number]['tomogram_resolutions'][number] export interface DownloadModalContextValue { - activeAnnotation?: Annotation | null + activeAnnotation?: BaseAnnotation | null activeTomogramResolution?: TomogramResolution | null allTomogramProcessing?: string[] allTomogramResolutions?: TomogramResolution[] diff --git a/frontend/packages/data-portal/app/graphql/getRunById.server.ts b/frontend/packages/data-portal/app/graphql/getRunById.server.ts index dabddedfd..b7a3b496a 100644 --- a/frontend/packages/data-portal/app/graphql/getRunById.server.ts +++ b/frontend/packages/data-portal/app/graphql/getRunById.server.ts @@ -131,69 +131,6 @@ const GET_RUN_BY_ID_QUERY = gql(` } } - annotation_table: tomogram_voxel_spacings { - annotations( - limit: $limit, - offset: $annotationsOffset, - where: { - _and: $filter - }, - order_by: [ - { ground_truth_status: desc } - { deposition_date: desc } - { id: desc } - ], - ) { - annotation_method - annotation_publication - annotation_software - confidence_precision - confidence_recall - deposition_date - ground_truth_status - ground_truth_used - id - is_curator_recommended - last_modified_date - method_type - object_count - object_description - object_id - object_name - object_state - release_date - - files( - where: { - _and: $fileFilter - } - ) { - format - https_path - s3_path - shape_type - } - - authors(order_by: { author_list_order: asc }) { - primary_author_status - corresponding_author_status - name - email - orcid - } - - author_affiliations: authors(distinct_on: affiliation_name) { - affiliation_name - } - - authors_aggregate { - aggregate { - count - } - } - } - } - tomogram_stats: tomogram_voxel_spacings { annotations { object_name @@ -242,6 +179,91 @@ const GET_RUN_BY_ID_QUERY = gql(` } } + # Annotations table: + annotation_files( + where: { + format: { + _neq: "zarr" # TODO: Remove hack, migrate to new annotation + shape object. + } + annotation: { + tomogram_voxel_spacing: { + run_id: { + _eq: $id + } + } + _and: $filter + } + _and: $fileFilter + } + order_by: [ + { + annotation: { + ground_truth_status: desc + } + }, + { + annotation: { + deposition_date: desc + } + }, + { + annotation_id: desc + } + ] + limit: $limit + offset: $annotationsOffset + ) { + shape_type + format + + annotation { + annotation_method + annotation_publication + annotation_software + confidence_precision + confidence_recall + deposition_date + ground_truth_status + ground_truth_used + id + is_curator_recommended + last_modified_date + method_type + object_count + object_description + object_id + object_name + object_state + release_date + + files(where: { _and: $fileFilter }) { + shape_type + format + https_path + s3_path + } + + authors(order_by: { author_list_order: asc }) { + primary_author_status + corresponding_author_status + name + email + orcid + } + + author_affiliations: authors(distinct_on: affiliation_name) { + affiliation_name + } + + authors_aggregate { + aggregate { + count + } + } + } + } + + # Annotation counts: annotation_files_aggregate_for_total: annotation_files_aggregate( where: { annotation: { @@ -258,7 +280,6 @@ const GET_RUN_BY_ID_QUERY = gql(` count } } - annotation_files_aggregate_for_filtered: annotation_files_aggregate( where: { annotation: { @@ -277,7 +298,6 @@ const GET_RUN_BY_ID_QUERY = gql(` count } } - annotation_files_aggregate_for_ground_truth: annotation_files_aggregate( where: { annotation: { @@ -299,7 +319,6 @@ const GET_RUN_BY_ID_QUERY = gql(` count } } - annotation_files_aggregate_for_other: annotation_files_aggregate( where: { annotation: { @@ -349,13 +368,8 @@ function getFilter(filterState: FilterState): Annotations_Bool_Exp[] { }) } - const { - objectNames, - objectShapeTypes, - annotationSoftwares, - methodTypes, - goId, - } = filterState.annotation + const { objectNames, annotationSoftwares, methodTypes, goId } = + filterState.annotation if (objectNames.length > 0) { filters.push({ @@ -373,16 +387,6 @@ function getFilter(filterState: FilterState): Annotations_Bool_Exp[] { }) } - if (objectShapeTypes.length > 0) { - filters.push({ - files: { - shape_type: { - _in: objectShapeTypes, - }, - }, - }) - } - if (methodTypes.length > 0) { filters.push({ method_type: { diff --git a/frontend/packages/data-portal/app/hooks/useRunById.ts b/frontend/packages/data-portal/app/hooks/useRunById.ts index 69cc4487f..ca110d700 100644 --- a/frontend/packages/data-portal/app/hooks/useRunById.ts +++ b/frontend/packages/data-portal/app/hooks/useRunById.ts @@ -8,6 +8,8 @@ export function useRunById() { const run = data.runs[0] + const annotationFiles = data.annotation_files + const objectNames = useMemo( () => Array.from( @@ -61,6 +63,7 @@ export function useRunById() { return { run, + annotationFiles, objectNames, objectShapeTypes, annotationSoftwares, diff --git a/frontend/packages/data-portal/app/routes/runs.$id.tsx b/frontend/packages/data-portal/app/routes/runs.$id.tsx index b92f02015..f8208f247 100644 --- a/frontend/packages/data-portal/app/routes/runs.$id.tsx +++ b/frontend/packages/data-portal/app/routes/runs.$id.tsx @@ -2,7 +2,7 @@ import { ShouldRevalidateFunctionArgs } from '@remix-run/react' import { json, LoaderFunctionArgs } from '@remix-run/server-runtime' -import { isNumber } from 'lodash-es' +import { isNumber, toNumber } from 'lodash-es' import { useMemo } from 'react' import { match } from 'ts-pattern' @@ -20,7 +20,7 @@ import { useDownloadModalQueryParamState } from 'app/hooks/useDownloadModalQuery import { useFileSize } from 'app/hooks/useFileSize' import { useI18n } from 'app/hooks/useI18n' import { useRunById } from 'app/hooks/useRunById' -import { Annotation } from 'app/state/annotation' +import { BaseAnnotation } from 'app/state/annotation' import { DownloadConfig } from 'app/types/download' import { useFeatureFlag } from 'app/utils/featureFlags' import { shouldRevalidatePage } from 'app/utils/revalidate' @@ -75,8 +75,7 @@ export function shouldRevalidate(args: ShouldRevalidateFunctionArgs) { export default function RunByIdPage() { const multipleTomogramsEnabled = useFeatureFlag('multipleTomograms') - - const { run, annotationFilesAggregates } = useRunById() + const { run, annotationFiles, annotationFilesAggregates } = useRunById() const allTomogramResolutions = run.tomogram_stats.flatMap((stats) => stats.tomogram_resolutions.map((tomogram) => tomogram), @@ -107,33 +106,13 @@ export default function RunByIdPage() { const tomogram = run.tomogram_voxel_spacings.at(0) const { t } = useI18n() - const activeAnnotation = useMemo(() => { - const allAnnotations = new Map( - run.annotation_table - .flatMap((table) => table.annotations.map((annotation) => annotation)) - .map((annotation) => [annotation.id, annotation]), - ) - - const activeBaseAnnotation = annotationId - ? allAnnotations.get(+annotationId) - : null - - const activeAnnotationFile = objectShapeType - ? activeBaseAnnotation?.files.find( - (file) => file.shape_type === objectShapeType, - ) - : null - - const result = - activeBaseAnnotation && activeAnnotationFile - ? { - ...activeBaseAnnotation, - ...activeAnnotationFile, - } - : null - - return result as Annotation | null - }, [annotationId, objectShapeType, run.annotation_table]) + const activeAnnotation: BaseAnnotation | undefined = useMemo( + () => + annotationFiles.find( + (file) => file.annotation.id === toNumber(annotationId), + )?.annotation, + [annotationId, annotationFiles], + ) const httpsPath = useMemo(() => { if (activeAnnotation) { diff --git a/frontend/packages/data-portal/app/state/annotation.ts b/frontend/packages/data-portal/app/state/annotation.ts index d3926b252..0639a5a88 100644 --- a/frontend/packages/data-portal/app/state/annotation.ts +++ b/frontend/packages/data-portal/app/state/annotation.ts @@ -4,14 +4,13 @@ import { useMemo } from 'react' import { GetRunByIdQuery } from 'app/__generated__/graphql' export type BaseAnnotation = - GetRunByIdQuery['runs'][number]['annotation_table'][number]['annotations'][number] + GetRunByIdQuery['annotation_files'][number]['annotation'] -export type AnnotationFile = - GetRunByIdQuery['runs'][number]['annotation_table'][number]['annotations'][number]['files'][number] +export type AnnotationFile = GetRunByIdQuery['annotation_files'][number] -export type Annotation = BaseAnnotation & AnnotationFile +export type AnnotationRow = BaseAnnotation & Omit -const activeAnnotationAtom = atom(null) +const activeAnnotationAtom = atom(null) export function useAnnotation() { const [activeAnnotation, setActiveAnnotation] = useAtom(activeAnnotationAtom) diff --git a/frontend/packages/data-portal/app/utils/annotation.ts b/frontend/packages/data-portal/app/utils/annotation.ts index 4f01c175a..13199dd05 100644 --- a/frontend/packages/data-portal/app/utils/annotation.ts +++ b/frontend/packages/data-portal/app/utils/annotation.ts @@ -1,6 +1,8 @@ -import { Annotation } from 'app/state/annotation' +import { AnnotationRow } from 'app/state/annotation' -export function getAnnotationTitle(annotation: Annotation | undefined | null) { +export function getAnnotationTitle( + annotation: AnnotationRow | undefined | null, +) { if (!annotation) { return '--' } diff --git a/frontend/packages/data-portal/e2e/filters/utils.ts b/frontend/packages/data-portal/e2e/filters/utils.ts index c07c18842..ac5de8e04 100644 --- a/frontend/packages/data-portal/e2e/filters/utils.ts +++ b/frontend/packages/data-portal/e2e/filters/utils.ts @@ -91,10 +91,7 @@ export function getAnnotationTableFilterValidator( expectedData: GetRunByIdQuery, ) { const annotationIds = new Set( - expectedData.runs - .at(0) - ?.annotation_table.at(0) - ?.annotations.map((annotation) => annotation.id), + expectedData.annotation_files.map((file) => file.annotation.id), ) return async (page: Page) => { diff --git a/frontend/packages/data-portal/e2e/pageObjects/filters/utils.ts b/frontend/packages/data-portal/e2e/pageObjects/filters/utils.ts index dab5bd2d8..6845783c4 100644 --- a/frontend/packages/data-portal/e2e/pageObjects/filters/utils.ts +++ b/frontend/packages/data-portal/e2e/pageObjects/filters/utils.ts @@ -77,17 +77,13 @@ export function getAnnotationRowCountFromData({ singleRunData: GetRunByIdQuery }): RowCounterType { const rowCounter: RowCounterType = {} - singleRunData.runs - .at(0) - ?.annotation_table.at(0) - ?.annotations.reduce((counter, annotation) => { - const objectShapeTypes = new Set() - for (const file of annotation.files) { - objectShapeTypes.add(file.shape_type) - } - counter[annotation.id] = objectShapeTypes.size - return counter - }, rowCounter) + for (const file of singleRunData.annotation_files) { + if (rowCounter[file.annotation.id] === undefined) { + rowCounter[file.annotation.id] = 1 + } else { + rowCounter[file.annotation.id] += 1 + } + } return rowCounter } // #endregion runPage diff --git a/frontend/packages/data-portal/e2e/pageObjects/metadataDrawer/utils.ts b/frontend/packages/data-portal/e2e/pageObjects/metadataDrawer/utils.ts index 33777180d..3941e0474 100644 --- a/frontend/packages/data-portal/e2e/pageObjects/metadataDrawer/utils.ts +++ b/frontend/packages/data-portal/e2e/pageObjects/metadataDrawer/utils.ts @@ -211,8 +211,7 @@ export async function getAnnotationTestData( annotationsPage: 1, }) - const [run] = data.runs - const annotation = run.annotation_table[0].annotations[0] + const { annotation } = data.annotation_files[0] return { title: `${annotation.id} - ${annotation.object_name}`, diff --git a/frontend/packages/eslint-config/typescript.cjs b/frontend/packages/eslint-config/typescript.cjs index f52450bd7..f470cea11 100644 --- a/frontend/packages/eslint-config/typescript.cjs +++ b/frontend/packages/eslint-config/typescript.cjs @@ -75,5 +75,15 @@ module.exports = { argsIgnorePattern: '^_', }, ], + + // Allow us to use the above naming pattern for destructuring unused variables. + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allow', + }, + ], }, }