Skip to content

Commit

Permalink
feat: annotation table updates (#481)
Browse files Browse the repository at this point in the history
#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

<img width="1728" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/9dc55804-956e-4766-8536-f50951f50c62">

### With Method Type

<img width="1728" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/696f5b96-efda-47fb-b9fb-2b0f98d88be5">
  • Loading branch information
codemonkey800 authored Feb 29, 2024
1 parent 7730efb commit 097e271
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function AnnotationObjectTable() {
},
{
label: t('objectShapeType'),
values: [annotation.shape_type],
values: [annotation.files[0].shape_type],
},
{
label: t('objectState'),
Expand Down
161 changes: 108 additions & 53 deletions frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 {
Expand All @@ -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>(() => ({
annotation_method: '',
Expand All @@ -30,13 +31,11 @@ const LOADING_ANNOTATIONS = range(0, MAX_PER_PAGE).map<Annotation>(() => ({
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 }) {
Expand All @@ -61,6 +60,8 @@ export function AnnotationTable() {
const { setActiveAnnotation } = useAnnotation()
const { t } = useI18n()

const { openTomogramDownloadModal } = useDownloadModalQueryParamState()

const openAnnotationDrawer = useCallback(
(annotation: Annotation) => {
setActiveAnnotation(annotation)
Expand All @@ -69,6 +70,8 @@ export function AnnotationTable() {
[toggleDrawer, setActiveAnnotation],
)

const showMethodType = useFeatureFlag('methodType')

const columns = useMemo(() => {
const columnHelper = createColumnHelper<Annotation>()

Expand Down Expand Up @@ -98,7 +101,7 @@ export function AnnotationTable() {
const value = getValue() as number | null

return (
<TableCell horizontalAlign="right" minWidth={85} maxWidth={120}>
<TableCell horizontalAlign="right" minWidth={81} maxWidth={120}>
{typeof value === 'number' ? (
<ConfidenceValue value={value} />
) : (
Expand All @@ -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 } }) => (
<TableCell
className="flex flex-col gap-sds-xxxs !items-start"
minWidth={250}
renderLoadingSkeleton={false}
>
<div className="flex gap-sds-xs">
<p className="text-sds-header-s leading-sds-header-s text-ellipsis line-clamp-1 break-all">
{getAnnotationTitle(annotation)}
<div className="flex gap-sds-xs items-center">
<p
className={cns(
'text-sds-body-m leading-sds-body-m font-semibold',
'text-ellipsis line-clamp-1 break-all',
)}
>
{annotation.id}
</p>

{annotation.ground_truth_status && (
Expand All @@ -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')}
Expand Down Expand Up @@ -177,43 +185,70 @@ export function AnnotationTable() {
),
}),

columnHelper.accessor('object_name', {
header: t('annotationObject'),
columnHelper.accessor('deposition_date', {
header: () => (
<CellHeader hideSortIcon className="whitespace-nowrap text-ellipsis">
{t('depositionDate')}
</CellHeader>
),

cell: ({ getValue }) => (
<TableCell minWidth={120} maxWidth={250}>
<TableCell minWidth={91} maxWidth={120}>
<div className="line-clamp-2 text-ellipsis capitalize">
{getValue()}
</div>
</TableCell>
),
}),

columnHelper.accessor('shape_type', {
header: () => (
<CellHeader horizontalAlign="right" hideSortIcon>
{t('objectShapeType')}
</CellHeader>
),
columnHelper.accessor('object_name', {
header: t('objectName'),
cell: ({ getValue }) => (
<TableCell horizontalAlign="right" minWidth={100} maxWidth={150}>
{getValue()}
<TableCell minWidth={120} maxWidth={250}>
<div className="line-clamp-2 text-ellipsis capitalize">
{getValue()}
</div>
</TableCell>
),
}),

columnHelper.accessor('object_count', {
header: () => (
<CellHeader horizontalAlign="right" hideSortIcon>
{t('objectCount')}
</CellHeader>
),
columnHelper.accessor('files', {
id: 'shape-type',
header: t('objectShapeType'),

cell: ({ getValue }) => (
<TableCell horizontalAlign="right" minWidth={85} maxWidth={120}>
{getValue()}
<TableCell minWidth={100} maxWidth={150}>
{getValue().at(0)?.shape_type ?? '--'}
</TableCell>
),
}),

...(showMethodType
? [
columnHelper.accessor('id', {
id: 'method-type',

header: () => (
<CellHeader className="whitespace-nowrap" hideSortIcon>
{t('methodType')}
</CellHeader>
),

cell: ({ row: { original: annotation } }) => (
<TableCell minWidth={81} maxWidth={120}>
<Button
sdsType="primary"
sdsStyle="minimal"
onClick={() => openAnnotationDrawer(annotation)}
>
{t('automated')}
</Button>
</TableCell>
),
}),
]
: []),

getConfidenceCell({
key: 'confidence_precision',
header: t('precision'),
Expand All @@ -239,30 +274,50 @@ export function AnnotationTable() {
// Render empty cell header so that it doesn't break the table layout
header: () => <CellHeader hideSortIcon>{null}</CellHeader>,
cell: ({ row: { original: annotation } }) => (
<TableCell minWidth={85} maxWidth={100}>
<Button
sdsType="primary"
sdsStyle="minimal"
onClick={() => openAnnotationDrawer(annotation)}
>
{t('moreInfo')}
</Button>
<TableCell minWidth={120} maxWidth={120}>
<div className="flex flex-col gap-sds-xs">
<Button
sdsType="primary"
sdsStyle="minimal"
onClick={() => openAnnotationDrawer(annotation)}
startIcon={
<Icon sdsIcon="infoCircle" sdsSize="s" sdsType="button" />
}
>
<span>{t('moreInfo')}</span>
</Button>

<Button
sdsType="primary"
sdsStyle="minimal"
onClick={() =>
openTomogramDownloadModal({
datasetId: run.dataset.id,
runId: run.id,
})
}
startIcon={
<Icon sdsIcon="download" sdsSize="s" sdsType="button" />
}
>
{t('download')}
</Button>
</div>
</TableCell>
),
}),
] as ColumnDef<Annotation>[]
}, [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<Annotation[]>(
() => run.annotation_table.flatMap((data) => data.annotations),
[run.annotation_table],
)

Expand Down
11 changes: 10 additions & 1 deletion frontend/packages/data-portal/app/graphql/getRunById.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 1 addition & 5 deletions frontend/packages/data-portal/app/state/annotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Annotation | null>(null)

export function useAnnotation() {
Expand Down
6 changes: 5 additions & 1 deletion frontend/packages/data-portal/app/utils/annotation.ts
Original file line number Diff line number Diff line change
@@ -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}`
}
45 changes: 45 additions & 0 deletions frontend/packages/data-portal/app/utils/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -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,
})
}
Loading

0 comments on commit 097e271

Please sign in to comment.