Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions src/components/data_library/LibraryFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ const CHECKBOX_FILTER_KEYS = [
'dataUseModifiers',
'dataType',
'dac',
'modelFormat',
'modelLicense',
'modelCloud',
'modelTags',
'workspaceTools',
'workspacePlatform',
'workspaceCloud',
'workspaceAccess',
'clinicalTrialStatus',
'clinicalTrialPhase',
'clinicalTrialInterventionType',
Expand All @@ -42,6 +48,14 @@ const CHECKBOX_FILTER_KEYS = [
'biospecimenDataUse',
'biospecimenPostMortemIntervalUnit',
'soApprovalModel',
'ipType',
'ipStatus',
'presentationEvent',
'presentationFormat',
'presentationAccess',
'publicationJournal',
'publicationAccess',
'fundingFunderName',
] as const

type CheckboxFilterKey = (typeof CHECKBOX_FILTER_KEYS)[number]
Expand Down Expand Up @@ -130,6 +144,20 @@ const DATE_SECTION_CONFIG = {
],
invertedMessage: 'Start Date cannot be after End Date',
},
presentationDate: {
fields: [
{ stateKey: 'before', label: 'Presented Before' },
{ stateKey: 'after', label: 'Presented After' },
],
invertedMessage: 'Presented After cannot be later than Presented Before',
},
publicationPublishedDate: {
fields: [
{ stateKey: 'before', label: 'Published Before' },
{ stateKey: 'after', label: 'Published After' },
],
invertedMessage: 'Published After cannot be later than Published Before',
},
} as const

type DateFilterSectionKey = keyof typeof DATE_SECTION_CONFIG
Expand Down Expand Up @@ -202,8 +230,6 @@ const DateFilterField: React.FC<DateFilterFieldProps> = ({ label, value, error,
// Yes/No/Any radio groups. A registered key claimed by neither this list nor
// CHECKBOX_FILTER_KEYS renders nothing at all.
const BOOLEAN_FILTER_KEYS = [
'datasetsCited',
'publicationsDatasetsCited',
'instantApproval',
] as const

Expand Down Expand Up @@ -625,6 +651,8 @@ export const LibraryFilters: React.FC<LibraryFiltersProps> = React.memo(({
|| section.key === 'biospecimenCollectionDate'
|| section.key === 'ipFiledDate'
|| section.key === 'fundingDate'
|| section.key === 'presentationDate'
|| section.key === 'publicationPublishedDate'
) {
return renderDateSection(section.key, section.label)
}
Expand Down
8 changes: 8 additions & 0 deletions src/components/data_library/assets/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ export const STUDIES_AGG: AggregationDefinition = {
},
}

/** Documents indexed before these fields became multi-selects still hold a bare string. */
export const toStringArray = (value: unknown): string[] => {
if (Array.isArray(value)) {
return value.filter((entry): entry is string => typeof entry === 'string')
}
return typeof value === 'string' && value ? [value] : []
}

/** Union of every row type that can appear in the DataGrid */
export type LibraryRow = DatasetTerm | StudyAggregation | ModelAsset | WorkspaceAsset | ClinicalTrialAsset | BiospecimenAsset | PublicationAsset | PresentationAsset | IntellectualPropertyAsset | FundingResourceAsset

Expand Down
16 changes: 9 additions & 7 deletions src/components/data_library/assets/fundingResourceAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ const matchesFundingResourceFilters = (funding: FundingResourceAsset, filters?:

// Inverted bounds build no ES clause, so they must not narrow rows here
// either — otherwise the grid empties while the panel flags the range.
if (!isFilterActive('fundingDate', filters)) {
return true
if (isFilterActive('fundingDate', filters)) {
const { startDate, endDate } = filters.fundingDate
if (startDate && (!funding.startDate || funding.startDate < startDate)) {
return false
}
if (endDate && (!funding.endDate || funding.endDate > endDate)) {
return false
}
}

const { startDate, endDate } = filters.fundingDate
if (startDate && (!funding.startDate || funding.startDate < startDate)) {
return false
}
return !(endDate && (!funding.endDate || funding.endDate > endDate))
return filters.fundingFunderName.length === 0 || filters.fundingFunderName.includes(funding.funderName)
}

export const fundingResourceAsset: AssetDefinition = {
Expand Down
18 changes: 13 additions & 5 deletions src/components/data_library/assets/intellectualPropertyAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,23 @@ const matchesIntellectualPropertyFilters = (ip: IntellectualPropertyAsset, filte

// Inverted bounds build no ES clause, so they must not narrow rows here
// either — otherwise the grid empties while the panel flags the range.
if (!isFilterActive('ipFiledDate', filters)) {
return true
if (isFilterActive('ipFiledDate', filters)) {
// A missing date matches neither bound, as in the ES range clause, which
// never matches a document without the field.
const { after, before } = filters.ipFiledDate
if (after && (!ip.filingDate || ip.filingDate < after)) {
return false
}
if (before && (!ip.filingDate || ip.filingDate > before)) {
return false
}
}

const filingDate = ip.filingDate || ''
if (filters.ipFiledDate.after && filingDate < filters.ipFiledDate.after) {
if (filters.ipType.length > 0 && !filters.ipType.includes(ip.type)) {
return false
}
return !(filters.ipFiledDate.before && filingDate > filters.ipFiledDate.before)

return filters.ipStatus.length === 0 || filters.ipStatus.includes(ip.status)
}

export const intellectualPropertyAsset: AssetDefinition = {
Expand Down
43 changes: 36 additions & 7 deletions src/components/data_library/assets/modelAsset.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import { GridColDef } from '@mui/x-data-grid'
import { ElasticsearchQuery, ElasticsearchResponse, ModelStudyAggregationResponse, QueryClause } from 'src/types/elastic'
import { ModelAsset, PaginationState, SortState } from 'src/types/library'
import { FilterState, ModelAsset, PaginationState, SortState } from 'src/types/library'
import { makeModelColumns } from 'src/components/data_library/columns/modelColumns'
import { AssetDefinition, ColumnsProps, LibraryPage, LibraryRow, STUDIES_AGG } from 'src/components/data_library/assets/definition'
import { AssetDefinition, ColumnsProps, LibraryPage, LibraryRow, STUDIES_AGG, toStringArray } from 'src/components/data_library/assets/definition'

// The Elasticsearch clauses for these filters only decide which *studies* enter
// the shared aggregation; every model of a qualifying study comes back, so each
// row must be re-checked here or the grid (and the tab-count badge derived from
// this same function) includes models that don't match the filter.
const matchesModelFilters = (model: ModelAsset, filters?: FilterState) => {
if (!filters) {
return true
}

if (filters.modelFormat.length > 0 && !filters.modelFormat.includes(model.format)) {
return false
}

if (filters.modelLicense.length > 0 && !filters.modelLicense.includes(model.license)) {
return false
}

if (filters.modelCloud.length > 0 && !(model.cloud || []).some(cloud => filters.modelCloud.includes(cloud))) {
return false
}

return filters.modelTags.length === 0 || (model.tags || []).some(tag => filters.modelTags.includes(tag))
}

export const modelAsset: AssetDefinition = {
label: { singular: 'AI Model', plural: 'AI Models' },
Expand Down Expand Up @@ -44,7 +68,7 @@ export const modelAsset: AssetDefinition = {
}
},

transformResponse(response: ElasticsearchResponse, pagination: PaginationState): LibraryPage {
transformResponse(response: ElasticsearchResponse, pagination: PaginationState, filters?: FilterState): LibraryPage {
const studiesAgg = response.aggregations?.studies as ModelStudyAggregationResponse | undefined
const buckets = studiesAgg?.buckets || []
const models: ModelAsset[] = []
Expand All @@ -55,7 +79,7 @@ export const modelAsset: AssetDefinition = {
for (const [modelIndex, model] of studyModels.entries()) {
// modelId may be absent from the indexed document; fall back to a
// composite key so every row in the DataGrid has a unique id.
models.push({
const row: ModelAsset = {
modelId: model.modelId || `${bucket.key}-${modelIndex}`,
studyId: bucket.key,
studyName: studyData.studyName || '',
Expand All @@ -64,13 +88,18 @@ export const modelAsset: AssetDefinition = {
url: model.url || '',
format: model.format || '',
license: model.license || '',
trainedOnDatasets: model.trainedOnDatasets || [],
cloud: toStringArray(model.cloud),
trainedOnDatasets: toStringArray(model.trainedOnDatasets),
maintainer: {
name: model.maintainer?.name || '',
email: model.maintainer?.email || '',
},
tags: model.tags || [],
})
tags: toStringArray(model.tags),
}

if (matchesModelFilters(row, filters)) {
models.push(row)
}
}
}

Expand Down
29 changes: 27 additions & 2 deletions src/components/data_library/assets/presentationAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,42 @@ import { ElasticsearchQuery, ElasticsearchResponse, PresentationStudyAggregation
import { FilterState, PaginationState, PresentationAsset, SortState } from 'src/types/library'
import { makePresentationColumns } from 'src/components/data_library/columns/presentationColumns'
import { AssetDefinition, ColumnsProps, LibraryPage, LibraryRow, STUDIES_AGG } from 'src/components/data_library/assets/definition'
import { isFilterActive } from 'src/components/data_library/filterRegistry'

// The Elasticsearch clauses for these filters only decide which *studies* enter
// the shared aggregation; every presentation of a qualifying study comes back,
// so each row must be re-checked here or the grid (and the tab-count badge
// derived from this same function) includes presentations that don't match.
const matchesPresentationFilters = (presentation: PresentationAsset, filters?: FilterState) => {
if (!filters) {
return true
}

if (filters.datasetsCited === undefined) {
if (filters.presentationEvent.length > 0 && !filters.presentationEvent.includes(presentation.event || '')) {
return false
}

if (filters.presentationFormat.length > 0 && !filters.presentationFormat.includes(presentation.format || '')) {
return false
}

if (filters.presentationAccess.length > 0 && !filters.presentationAccess.includes(presentation.access || '')) {
return false
}

// Inverted bounds build no ES clause, so they must not narrow rows here
// either — otherwise the grid empties while the panel flags the range.
if (!isFilterActive('presentationDate', filters)) {
return true
}

return presentation.citation === filters.datasetsCited
// A missing date matches neither bound, as in the ES range clause, which
// never matches a document without the field.
const { after, before } = filters.presentationDate
if (after && (!presentation.date || presentation.date < after)) {
return false
}
return !(before && (!presentation.date || presentation.date > before))
}

export const presentationAsset: AssetDefinition = {
Expand Down
25 changes: 23 additions & 2 deletions src/components/data_library/assets/publicationAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,38 @@ import { ElasticsearchQuery, ElasticsearchResponse, PublicationStudyAggregationR
import { FilterState, PaginationState, PublicationAsset, SortState } from 'src/types/library'
import { makePublicationColumns } from 'src/components/data_library/columns/publicationColumns'
import { AssetDefinition, ColumnsProps, LibraryPage, LibraryRow, STUDIES_AGG } from 'src/components/data_library/assets/definition'
import { isFilterActive } from 'src/components/data_library/filterRegistry'

// The Elasticsearch clauses for these filters only decide which *studies* enter
// the shared aggregation; every publication of a qualifying study comes back,
// so each row must be re-checked here or the grid (and the tab-count badge
// derived from this same function) includes publications that don't match.
const matchesPublicationFilters = (publication: PublicationAsset, filters?: FilterState) => {
if (!filters) {
return true
}

if (filters.publicationsDatasetsCited === undefined) {
if (filters.publicationJournal.length > 0 && !filters.publicationJournal.includes(publication.journal || '')) {
return false
}

if (filters.publicationAccess.length > 0 && !filters.publicationAccess.includes(publication.access || '')) {
return false
}

// Inverted bounds build no ES clause, so they must not narrow rows here
// either — otherwise the grid empties while the panel flags the range.
if (!isFilterActive('publicationPublishedDate', filters)) {
return true
}

return publication.citation === filters.publicationsDatasetsCited
// A missing date matches neither bound, as in the ES range clause, which
// never matches a document without the field.
const { after, before } = filters.publicationPublishedDate
if (after && (!publication.publishedDate || publication.publishedDate < after)) {
return false
}
return !(before && (!publication.publishedDate || publication.publishedDate > before))
}

export const publicationAsset: AssetDefinition = {
Expand Down
17 changes: 13 additions & 4 deletions src/components/data_library/assets/workspaceAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GridColDef } from '@mui/x-data-grid'
import { ElasticsearchQuery, ElasticsearchResponse, WorkspaceStudyAggregationResponse, QueryClause } from 'src/types/elastic'
import { FilterState, WorkspaceAsset, PaginationState, SortState } from 'src/types/library'
import { makeWorkspaceColumns } from 'src/components/data_library/columns/workspaceColumns'
import { AssetDefinition, ColumnsProps, LibraryPage, LibraryRow, STUDIES_AGG } from 'src/components/data_library/assets/definition'
import { AssetDefinition, ColumnsProps, LibraryPage, LibraryRow, STUDIES_AGG, toStringArray } from 'src/components/data_library/assets/definition'

const includesIgnoreCase = (source: string | undefined, values: string[]) => {
if (values.length === 0) {
Expand All @@ -29,7 +29,15 @@ const matchesWorkspaceFilters = (workspace: WorkspaceAsset, filters?: FilterStat
return false
}

return includesIgnoreCase(workspace.platform, filters.workspacePlatform)
if (!includesIgnoreCase(workspace.platform, filters.workspacePlatform)) {
return false
}

if (filters.workspaceCloud.length > 0 && !(workspace.cloud || []).some(cloud => filters.workspaceCloud.includes(cloud))) {
return false
}

return filters.workspaceAccess.length === 0 || filters.workspaceAccess.includes(workspace.access || '')
}

export const workspaceAsset: AssetDefinition = {
Expand Down Expand Up @@ -89,9 +97,10 @@ export const workspaceAsset: AssetDefinition = {
platform: workspace.platform || '',
url: workspace.url || '',
description: workspace.description || '',
tools: workspace.tools || [],
tools: toStringArray(workspace.tools),
cloud: toStringArray(workspace.cloud),
access: workspace.access || '',
tags: workspace.tags || [],
tags: toStringArray(workspace.tags),
}

if (matchesWorkspaceFilters(row, filters)) {
Expand Down
29 changes: 4 additions & 25 deletions src/components/data_library/columns/modelColumns.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react'
import { GridColDef } from '@mui/x-data-grid'
import { Link, Chip, Box, Tooltip } from '@mui/material'
import { Link, Box, Tooltip } from '@mui/material'
import { Link as RouterLink } from 'react-router'
import { ModelAsset } from 'src/types/library'
import { validateHttpUrl } from 'src/utils/UrlUtils'
import { chipListColumn } from 'src/components/data_library/columns/sharedColumns'

/**
* Column definitions for AI model view
Expand Down Expand Up @@ -84,6 +85,7 @@ export const makeModelColumns = (): GridColDef<ModelAsset>[] => [
)
},
},
chipListColumn<ModelAsset>('cloud', 'Cloud', row => row.cloud || [], 130),
{
field: 'maintainer',
headerName: 'Maintainer',
Expand Down Expand Up @@ -129,28 +131,5 @@ export const makeModelColumns = (): GridColDef<ModelAsset>[] => [
: null
},
},
{
field: 'tags',
headerName: 'Tags',
flex: 1,
minWidth: 150,
sortable: false,
valueGetter: (_value, row) => (row.tags || []).join(', '),
renderCell: (params) => {
const tags = params.row.tags || []
if (tags.length === 0) return null
return (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{tags.slice(0, 3).map((tag, i) => (
<Chip key={i} label={tag} size="small" variant="outlined" />
))}
{tags.length > 3 && (
<Tooltip title={tags.slice(3).join(', ')}>
<Chip label={`+${tags.length - 3}`} size="small" variant="outlined" />
</Tooltip>
)}
</Box>
)
},
},
chipListColumn<ModelAsset>('tags', 'Tags', row => row.tags || [], 150),
]
Loading
Loading