Skip to content

Commit

Permalink
Implement browse all filters (#158)
Browse files Browse the repository at this point in the history
#25

Implements the browse all filters component for the browse all page.
Overall the implementation of the filters works keeping state in sync
with the query parameters. This is ideal because it:

1. Makes the application "stateless"
- Even though state is stored in the query parameters, the application
itself does not rely on any local state to have the filters working
1. Allows us to create permalinks of the filter page with every filter
specified
- Since the data is stored in the URL, opening the URL in another tab
will init the local data with the query parameters, effectively creating
a permalink
1. Leverages browser history API
- This will allow us to go back / forward in the browser since the
filter state is stored in query parameters

The components implemented in `components/Filters` should be generic
enough to be used in other places as well, so it should be
straightforward to re-use the components when we implement filters on
other pages.

## Demo

### Filters demo


https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/74db52b9-ddb6-4819-9409-79a7c3e33281

### No results page

<img width="565" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/18b613c2-a09e-4e3d-9504-1187e0296347">
  • Loading branch information
codemonkey800 authored Nov 21, 2023
1 parent ed7b04e commit f85c4fe
Show file tree
Hide file tree
Showing 44 changed files with 1,714 additions and 65 deletions.
6 changes: 2 additions & 4 deletions frontend/packages/data-portal/.stylelintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ rules:

selector-max-compound-selectors: null
selector-no-qualifying-type: null
'scss/function-no-unknown':

function-no-unknown:
- true
- ignoreFunctions:
- calculate-rem
- strip
- theme
- viewport-clamp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BrowseDataTabs } from './BrowseDataTabs'

export function BrowseDataHeader() {
return (
<div className="px-sds-xl py-sds-l flex justify-center border-b border-gray-300">
<div className="px-sds-xl py-sds-l flex justify-center">
<div className="flex gap-sds-xl w-full max-w-content">
<BrowseDataSearch />
<BrowseDataTabs />
Expand Down
7 changes: 4 additions & 3 deletions frontend/packages/data-portal/app/components/Dataset/type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DeepPartial } from 'utility-types'

import { Dataset_Authors, Datasets } from 'app/__generated__/graphql'
import { RecursivePartial } from 'app/utils/RecursivePartial'

export type DatasetType = RecursivePartial<Datasets> & {
authors_with_affiliation?: RecursivePartial<Dataset_Authors>[]
export type DatasetType = DeepPartial<Datasets> & {
authors_with_affiliation?: DeepPartial<Dataset_Authors>[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useSearchParams } from '@remix-run/react'
import { useMemo } from 'react'

import { FilterSection, SelectFilter } from 'app/components/Filters'
import { DatasetFilterQueryParams } from 'app/constants/query'
import { useDatasets } from 'app/hooks/useDatasets'
import { i18n } from 'app/i18n'
import { BaseFilterOption } from 'app/types/filter'

export function AnnotationMetadataFilterSection() {
const { objectNames, objectShapeTypes } = useDatasets()
const [searchParams, setSearchParams] = useSearchParams()

const objectNameOptions = useMemo(
() => objectNames.map<BaseFilterOption>((value) => ({ value })),
[objectNames],
)

const objectNameValue = useMemo<BaseFilterOption[]>(
() =>
searchParams
.getAll(DatasetFilterQueryParams.ObjectName)
.map((value) => ({ value })),
[searchParams],
)

const objectShapeTypeOptions = useMemo(
() => objectShapeTypes.map<BaseFilterOption>((value) => ({ value })),
[objectShapeTypes],
)

const objectShapeTypeValue = useMemo<BaseFilterOption[]>(
() =>
searchParams
.getAll(DatasetFilterQueryParams.ObjectShapeType)
.map((value) => ({ value })),
[searchParams],
)

return (
<FilterSection title={i18n.annotationMetadata}>
<SelectFilter
multiple
options={objectNameOptions}
value={objectNameValue}
label={i18n.objectName}
onChange={(options) =>
setSearchParams((prev) => {
prev.delete(DatasetFilterQueryParams.ObjectName)

options?.forEach((option) =>
prev.append(DatasetFilterQueryParams.ObjectName, option.value),
)

return prev
})
}
/>

<SelectFilter
multiple
options={objectShapeTypeOptions}
value={objectShapeTypeValue}
label={i18n.objectShapeType}
onChange={(options) =>
setSearchParams((prev) => {
prev.delete(DatasetFilterQueryParams.ObjectShapeType)

options?.forEach((option) =>
prev.append(
DatasetFilterQueryParams.ObjectShapeType,
option.value,
),
)

return prev
})
}
/>
</FilterSection>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Filters } from 'app/components/Filters'
import { i18n } from 'app/i18n'

import { AnnotationMetadataFilterSection } from './AnnotationMetadataFilterSection'
import { HardwareFilterSection } from './HardwareFilterSection'
import { IncludedContentsFilterSection } from './IncludedContentsFilterSection'
import { NameOrIdFilterSection } from './NameOrIdFilterSection'
import { SampleAndExperimentFilterSection } from './SampleAndExperimentFilterSection'
import { TiltSeriesMetadataFilterSection } from './TiltSeriesMetadataFilterSection'
import { TomogramMetadataFilterSection } from './TomogramMetadataFilterSection'

export function DatasetFilter() {
return (
<Filters title={i18n.filterBy}>
<IncludedContentsFilterSection />
<NameOrIdFilterSection />
<SampleAndExperimentFilterSection />
<HardwareFilterSection />
<TiltSeriesMetadataFilterSection />
<TomogramMetadataFilterSection />
<AnnotationMetadataFilterSection />
</Filters>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useMemo } from 'react'

import { FilterSection, SelectFilter } from 'app/components/Filters'
import { DatasetFilterQueryParams } from 'app/constants/query'
import { useDatasetFilter } from 'app/hooks/useDatasetFilter'
import { useDatasets } from 'app/hooks/useDatasets'
import { i18n } from 'app/i18n'
import { BaseFilterOption } from 'app/types/filter'

export function HardwareFilterSection() {
const { cameraManufacturers } = useDatasets()

const cameraManufacturerOptions = useMemo(
() => cameraManufacturers.map<BaseFilterOption>((value) => ({ value })),
[cameraManufacturers],
)

const {
updateValue,
hardware: { cameraManufacturer },
} = useDatasetFilter()

const cameraManufacturerValue = useMemo<BaseFilterOption | null>(() => {
return cameraManufacturer ? { value: cameraManufacturer } : null
}, [cameraManufacturer])

return (
<FilterSection title={i18n.hardware}>
<SelectFilter
options={cameraManufacturerOptions}
value={cameraManufacturerValue}
label={i18n.cameraManufacturer}
onChange={(option) =>
updateValue(DatasetFilterQueryParams.CameraManufacturer, option)
}
/>
</FilterSection>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useMemo } from 'react'

import {
BooleanFilter,
FilterSection,
SelectFilter,
} from 'app/components/Filters'
import { DatasetFilterQueryParams } from 'app/constants/query'
import { useDatasetFilter } from 'app/hooks/useDatasetFilter'
import { i18n } from 'app/i18n'
import {
AvailableFilesFilterOption,
NumberOfRunsFilterOption,
} from 'app/types/filter'

const NUMBER_OF_RUN_OPTIONS: NumberOfRunsFilterOption[] = [
{ value: '>1' },
{ value: '>5' },
{ value: '>10' },
{ value: '>20' },
{ value: '>100' },
]

const AVAILABLE_FILES_OPTIONS: AvailableFilesFilterOption[] = [
{ value: 'raw-frames', label: i18n.rawFrames },
{ value: 'tilt-series', label: i18n.tiltSeries },
{ value: 'tilt-series-alignment', label: i18n.tiltSeriesAlignment },
{ value: 'tomogram', label: i18n.tomogram },
]

export function IncludedContentsFilterSection() {
const {
updateValue,
includedContents: { isGroundTruthEnabled, availableFiles, numberOfRuns },
} = useDatasetFilter()

const availableFilesOptions = useMemo(
() =>
availableFiles
.map(
(option) =>
AVAILABLE_FILES_OPTIONS.find(({ value }) => value === option) ??
null,
)
.filter((option): option is AvailableFilesFilterOption => !!option),
[availableFiles],
)

const numberOfRunsOptions = useMemo(
() =>
numberOfRuns
? NUMBER_OF_RUN_OPTIONS.find(({ value }) => value === numberOfRuns) ??
null
: null,
[numberOfRuns],
)

return (
<FilterSection title={i18n.includedContents}>
<BooleanFilter
label={i18n.groundTruthAnnotation}
onChange={(value) =>
updateValue(
DatasetFilterQueryParams.GroundTruthAnnotation,
value ? 'true' : null,
)
}
value={isGroundTruthEnabled}
/>

<SelectFilter
multiple
options={AVAILABLE_FILES_OPTIONS}
value={availableFilesOptions}
label={i18n.availableFiles}
onChange={(options) =>
updateValue(DatasetFilterQueryParams.AvailableFiles, options)
}
/>

<SelectFilter
options={NUMBER_OF_RUN_OPTIONS}
value={numberOfRunsOptions}
label={i18n.numberOfRuns}
onChange={(option) =>
updateValue(
DatasetFilterQueryParams.NumberOfRuns,
option ? JSON.stringify(option.value) : null,
)
}
/>
</FilterSection>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
FilterSection,
InputFilterData,
MultiInputFilter,
} from 'app/components/Filters'
import { DatasetFilterQueryParams } from 'app/constants/query'
import { i18n } from 'app/i18n'

const DATASET_ID_FILTERS: InputFilterData[] = [
{
id: 'portal-id-input',
label: `${i18n.portalIdBlank}:`,
queryParam: DatasetFilterQueryParams.PortalId,
},
{
id: 'empiar-id-input',
label: `${i18n.empiarID}:`,
queryParam: DatasetFilterQueryParams.EmpiarId,
},
{
id: 'emdb-id-input',
label: `${i18n.emdb}:`,
queryParam: DatasetFilterQueryParams.EmdbId,
},
]

const AUTHOR_FILTERS: InputFilterData[] = [
{
id: 'author-name-input',
label: `${i18n.authorName}:`,
queryParam: DatasetFilterQueryParams.AuthorName,
},
{
id: 'author-orcid-input',
label: `${i18n.authorOrcid}:`,
queryParam: DatasetFilterQueryParams.AuthorOrcid,
},
]

export function NameOrIdFilterSection() {
return (
<FilterSection title={i18n.nameOrId}>
<MultiInputFilter label={i18n.datasetIds} filters={DATASET_ID_FILTERS} />
<MultiInputFilter label={i18n.author} filters={AUTHOR_FILTERS} />
</FilterSection>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useMemo } from 'react'

import { FilterSection, SelectFilter } from 'app/components/Filters'
import { DatasetFilterQueryParams } from 'app/constants/query'
import { useDatasetFilter } from 'app/hooks/useDatasetFilter'
import { useDatasets } from 'app/hooks/useDatasets'
import { i18n } from 'app/i18n'
import { BaseFilterOption } from 'app/types/filter'

export function SampleAndExperimentFilterSection() {
const {
updateValue,
sampleAndExperimentConditions: { organismNames },
} = useDatasetFilter()
const { organismNames: allOrganismNames } = useDatasets()

const organismNameOptions = useMemo(
() => allOrganismNames.map<BaseFilterOption>((name) => ({ value: name })),
[allOrganismNames],
)

const organismNameValue = useMemo(
() => organismNames.map<BaseFilterOption>((value) => ({ value })),
[organismNames],
)

return (
<FilterSection title={i18n.sampleAndExperimentConditions}>
<SelectFilter
multiple
search
options={organismNameOptions}
value={organismNameValue}
label={i18n.organismName}
onChange={(options) =>
updateValue(DatasetFilterQueryParams.Organism, options)
}
/>
</FilterSection>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { FilterSection, TiltRangeFilter } from 'app/components/Filters'
import { i18n } from 'app/i18n'

export function TiltSeriesMetadataFilterSection() {
return (
<FilterSection title={i18n.tiltSeriesMetadata}>
<TiltRangeFilter />
</FilterSection>
)
}
Loading

0 comments on commit f85c4fe

Please sign in to comment.