From 097e2716e246d39e3d36791dacd178bd42a86d9d Mon Sep 17 00:00:00 2001 From: Jeremy Asuncion Date: Wed, 28 Feb 2024 21:32:32 -0800 Subject: [PATCH] feat: annotation table updates (#481) #432 - Adds rudimentary feature flagging module that can be controlled via code or query parameter - Updates the annotation table to match the updated design - Implements but hides method type column ## Demo https://dev-jeremy-anno-table.cryoet.dev.si.czi.technology/runs/242 image ### With Method Type image --- .../components/Run/AnnotationObjectTable.tsx | 2 +- .../app/components/Run/AnnotationTable.tsx | 161 ++++++++++++------ .../app/graphql/getRunById.server.ts | 11 +- .../data-portal/app/state/annotation.ts | 6 +- .../data-portal/app/utils/annotation.ts | 6 +- .../data-portal/app/utils/featureFlags.ts | 45 +++++ .../public/locales/en/translation.json | 5 + 7 files changed, 175 insertions(+), 61 deletions(-) create mode 100644 frontend/packages/data-portal/app/utils/featureFlags.ts diff --git a/frontend/packages/data-portal/app/components/Run/AnnotationObjectTable.tsx b/frontend/packages/data-portal/app/components/Run/AnnotationObjectTable.tsx index 6b6b82432..03d6d27f7 100644 --- a/frontend/packages/data-portal/app/components/Run/AnnotationObjectTable.tsx +++ b/frontend/packages/data-portal/app/components/Run/AnnotationObjectTable.tsx @@ -35,7 +35,7 @@ export function AnnotationObjectTable() { }, { label: t('objectShapeType'), - values: [annotation.shape_type], + values: [annotation.files[0].shape_type], }, { label: t('objectState'), diff --git a/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx b/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx index f09144f2c..7309cc229 100644 --- a/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx +++ b/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unstable-nested-components */ -import { Button } from '@czi-sds/components' +import { Button, Icon } from '@czi-sds/components' import { ColumnDef, createColumnHelper } from '@tanstack/react-table' import { range } from 'lodash-es' import { ComponentProps, useCallback, useMemo } from 'react' @@ -9,6 +9,7 @@ import { I18n } from 'app/components/I18n' import { CellHeader, PageTable, TableCell } from 'app/components/Table' import { Tooltip } from 'app/components/Tooltip' import { MAX_PER_PAGE } from 'app/constants/pagination' +import { useDownloadModalQueryParamState } from 'app/hooks/useDownloadModalQueryParamState' import { useI18n } from 'app/hooks/useI18n' import { useIsLoading } from 'app/hooks/useIsLoading' import { @@ -18,8 +19,8 @@ import { import { useRunById } from 'app/hooks/useRunById' import { Annotation, useAnnotation } from 'app/state/annotation' import { I18nKeys } from 'app/types/i18n' -import { getAnnotationTitle } from 'app/utils/annotation' -import { cnsNoMerge } from 'app/utils/cns' +import { cns, cnsNoMerge } from 'app/utils/cns' +import { useFeatureFlag } from 'app/utils/featureFlags' const LOADING_ANNOTATIONS = range(0, MAX_PER_PAGE).map(() => ({ annotation_method: '', @@ -30,13 +31,11 @@ const LOADING_ANNOTATIONS = range(0, MAX_PER_PAGE).map(() => ({ deposition_date: '', files: [], ground_truth_status: false, - https_path: '', + id: 0, object_count: 0, object_id: '', object_name: '', release_date: '', - s3_path: '', - shape_type: '', })) function ConfidenceValue({ value }: { value: number }) { @@ -61,6 +60,8 @@ export function AnnotationTable() { const { setActiveAnnotation } = useAnnotation() const { t } = useI18n() + const { openTomogramDownloadModal } = useDownloadModalQueryParamState() + const openAnnotationDrawer = useCallback( (annotation: Annotation) => { setActiveAnnotation(annotation) @@ -69,6 +70,8 @@ export function AnnotationTable() { [toggleDrawer, setActiveAnnotation], ) + const showMethodType = useFeatureFlag('methodType') + const columns = useMemo(() => { const columnHelper = createColumnHelper() @@ -98,7 +101,7 @@ export function AnnotationTable() { const value = getValue() as number | null return ( - + {typeof value === 'number' ? ( ) : ( @@ -113,17 +116,22 @@ export function AnnotationTable() { } return [ - columnHelper.accessor('s3_path', { - header: t('annotations'), + columnHelper.accessor('id', { + header: t('annotationId'), cell: ({ row: { original: annotation } }) => ( -
-

- {getAnnotationTitle(annotation)} +

+

+ {annotation.id}

{annotation.ground_truth_status && ( @@ -135,8 +143,8 @@ export function AnnotationTable() { className={cnsNoMerge( 'px-sds-xs py-sds-xxxs', 'flex items-center justify-center', - 'rounded-sds-m bg-sds-info-400', - 'text-sds-body-xxxs leading-sds-body-xxxs text-sds-gray-white whitespace-nowrap', + 'rounded-sds-m bg-sds-info-200', + 'text-sds-body-xxxs leading-sds-body-xxxs text-sds-info-600 whitespace-nowrap', )} > {t('groundTruth')} @@ -177,10 +185,15 @@ export function AnnotationTable() { ), }), - columnHelper.accessor('object_name', { - header: t('annotationObject'), + columnHelper.accessor('deposition_date', { + header: () => ( + + {t('depositionDate')} + + ), + cell: ({ getValue }) => ( - +
{getValue()}
@@ -188,32 +201,54 @@ export function AnnotationTable() { ), }), - columnHelper.accessor('shape_type', { - header: () => ( - - {t('objectShapeType')} - - ), + columnHelper.accessor('object_name', { + header: t('objectName'), cell: ({ getValue }) => ( - - {getValue()} + +
+ {getValue()} +
), }), - columnHelper.accessor('object_count', { - header: () => ( - - {t('objectCount')} - - ), + columnHelper.accessor('files', { + id: 'shape-type', + header: t('objectShapeType'), + cell: ({ getValue }) => ( - - {getValue()} + + {getValue().at(0)?.shape_type ?? '--'} ), }), + ...(showMethodType + ? [ + columnHelper.accessor('id', { + id: 'method-type', + + header: () => ( + + {t('methodType')} + + ), + + cell: ({ row: { original: annotation } }) => ( + + + + ), + }), + ] + : []), + getConfidenceCell({ key: 'confidence_precision', header: t('precision'), @@ -239,30 +274,50 @@ export function AnnotationTable() { // Render empty cell header so that it doesn't break the table layout header: () => {null}, cell: ({ row: { original: annotation } }) => ( - - + +
+ + + +
), }), ] as ColumnDef[] - }, [openAnnotationDrawer, t]) - - const annotations = useMemo( - () => - run.annotation_table.flatMap((data) => - data.annotations.flatMap((annotation) => - annotation.files.map((file) => ({ - ...annotation, - ...file, - })), - ), - ) as Annotation[], + }, [ + openAnnotationDrawer, + openTomogramDownloadModal, + run.dataset.id, + run.id, + showMethodType, + t, + ]) + + const annotations = useMemo( + () => run.annotation_table.flatMap((data) => data.annotations), [run.annotation_table], ) diff --git a/frontend/packages/data-portal/app/graphql/getRunById.server.ts b/frontend/packages/data-portal/app/graphql/getRunById.server.ts index d14af93af..6482420d1 100644 --- a/frontend/packages/data-portal/app/graphql/getRunById.server.ts +++ b/frontend/packages/data-portal/app/graphql/getRunById.server.ts @@ -115,7 +115,15 @@ const GET_RUN_BY_ID_QUERY = gql(` } annotation_table: tomogram_voxel_spacings { - annotations(limit: $limit, offset: $offset) { + annotations( + limit: $limit, + offset: $offset, + order_by: [ + { ground_truth_status: desc } + { deposition_date: desc } + { id: desc } + ], + ) { annotation_method annotation_publication annotation_software @@ -124,6 +132,7 @@ const GET_RUN_BY_ID_QUERY = gql(` deposition_date ground_truth_status ground_truth_used + id is_curator_recommended last_modified_date object_count diff --git a/frontend/packages/data-portal/app/state/annotation.ts b/frontend/packages/data-portal/app/state/annotation.ts index 8fd36009b..0b6b9e6e1 100644 --- a/frontend/packages/data-portal/app/state/annotation.ts +++ b/frontend/packages/data-portal/app/state/annotation.ts @@ -3,13 +3,9 @@ import { useMemo } from 'react' import { GetRunByIdQuery } from 'app/__generated__/graphql' -type RootAnnotation = +export type Annotation = GetRunByIdQuery['runs'][number]['annotation_table'][number]['annotations'][number] -type AnnotationFile = RootAnnotation['files'][number] - -export type Annotation = RootAnnotation & AnnotationFile - const activeAnnotationAtom = atom(null) export function useAnnotation() { diff --git a/frontend/packages/data-portal/app/utils/annotation.ts b/frontend/packages/data-portal/app/utils/annotation.ts index 8e236a93f..4f01c175a 100644 --- a/frontend/packages/data-portal/app/utils/annotation.ts +++ b/frontend/packages/data-portal/app/utils/annotation.ts @@ -1,5 +1,9 @@ import { Annotation } from 'app/state/annotation' export function getAnnotationTitle(annotation: Annotation | undefined | null) { - return annotation?.s3_path?.split('/').at(-1) ?? '--' + if (!annotation) { + return '--' + } + + return `${annotation.id} - ${annotation.object_name}` } diff --git a/frontend/packages/data-portal/app/utils/featureFlags.ts b/frontend/packages/data-portal/app/utils/featureFlags.ts new file mode 100644 index 000000000..9af08d62f --- /dev/null +++ b/frontend/packages/data-portal/app/utils/featureFlags.ts @@ -0,0 +1,45 @@ +import { useSearchParams } from '@remix-run/react' + +import { useEnvironment } from 'app/context/Environment.context' + +export type FeatureFlagEnvironment = typeof process.env.ENV + +export const FEATURE_FLAGS = { + methodType: 'dev', +} as const + +export type FeatureFlagKey = keyof typeof FEATURE_FLAGS + +const ENABLE_FEATURE_PARAM = 'enable-feature' +const DISABLE_FEATURE_PARAM = 'disable-feature' + +export function getFeatureFlag({ + env, + key, + params = new URLSearchParams(), +}: { + env: FeatureFlagEnvironment + key: FeatureFlagKey + params?: URLSearchParams +}): boolean { + if (params.getAll(DISABLE_FEATURE_PARAM).includes(key)) { + return false + } + + if (params.getAll(ENABLE_FEATURE_PARAM).includes(key)) { + return true + } + + return FEATURE_FLAGS[key] === env +} + +export function useFeatureFlag(key: FeatureFlagKey): boolean { + const [params] = useSearchParams() + const { ENV } = useEnvironment() + + return getFeatureFlag({ + key, + params, + env: ENV, + }) +} diff --git a/frontend/packages/data-portal/public/locales/en/translation.json b/frontend/packages/data-portal/public/locales/en/translation.json index dfa5ea165..762bb8fda 100644 --- a/frontend/packages/data-portal/public/locales/en/translation.json +++ b/frontend/packages/data-portal/public/locales/en/translation.json @@ -12,6 +12,7 @@ "annotatedObjects": "Annotated Objects", "annotationConfidence": "Annotation Confidence", "annotationDetails": "Annotation Details", + "annotationId": "Annotation ID", "annotationMetadata": "Annotation Metadata", "annotationMethod": "Annotation Method", "annotationObject": "Annotation Object", @@ -27,6 +28,7 @@ "authorOrcid": "Author ORCID", "authors": "Authors", "authorsMaybePlural": "Author(s)", + "automated": "Automated", "availableFiles": "Available Files", "availableProcessing": "Available Processing", "awsCliLink": "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html", @@ -114,6 +116,7 @@ "helpAndReport": "Help & Report", "helpUsAchieveThisVision": "Help us achieve this vision", "howToCite": "How to cite", + "hybrid": "Hybrid", "ifYouEncounterIssuesWithDownloadTime": "If you encounter issues with download time, we recommend downloading larger files via API.", "imageCorrector": "Image Corrector", "includedContents": "Included Contents", @@ -131,8 +134,10 @@ "lastUpdated": "Last Updated", "license": "License", "limitOneValuePerField": "Limit one value per field", + "manual": "Manual", "meetsAll": "Meets all", "metadata": "Metadata", + "methodType": "Method Type", "microscopeManufacturer": "Microscope Manufacturer", "microscopeModel": "Microscope model", "moderate": "Moderate",